Breakout (Standard-Version)
In diesem zweiten, größeren Projekt möchte ich alles zusammenbringen, was bisher über Shapes, Animationen und Kollisionen behandelt wurde. Ich möchte dafür zuerst das klassische Breakout-Spiel, ursprünglich ein Spielhallenklassiker, der in vielen Formen für viele Heimcomputer nachprogrammiert wurde, in Processing.py erstellen. Danach möchte ich eine Variation vorstellen, die jüngst Yining Shi als Gastdozentin bei Daniel Shiffman in P5.js, dem JavaScript-Mode von Processing, vorgestellt hat. Aber fangen wir erst einmal bei den Basics an:
Paddle and Ball
Das Spiel, das man auch als eine Solo-Variante von Pong sehen kann benötigt erst einmal einen Paddle, der mit den Pfeiltasten hin- und herbewegt werden kann und mit dem der Spieler verhindern soll, daß der Ball, der die Bricks treffen und damit zum Verschwinden bringen soll, nach unten ins Nirwana fällt. Denn fällt der Ball nach unten, hat der Spieler in der einfachsten Version verloren. Es gibt auch Versionen, in denen der Spieler mehrere Bälle hat und erst, nachdem er den letzten Ball nicht mehr mit dem Paddle zurückschlagen konnte, war das Spiel zu Ende.
Gewonnen hatte der Spieler, wenn er alle Bricks mit dem Ball weggeschlagen hat. Das ist nicht einfach, weil nicht jeder Stein verschwindet, wenn er getroffen wird. Manche Steine müssen zwei- oder dreimal getroffen werden, bevor ie verschwinden.
Setzen wir erst einmal unsere Spielwelt auf:
def setup():
size(605, 400)
def draw():
background(0, 0, 0)
Da passiert noch nicht viel, alles was wir haben, ist ein schwarzes Fenster. Die benötigten Klassen lade ich der Übersicht halber in eine separate Datei (einen separaten Tab) aus, den ich gameworld.py
genannt habe. Dort wird als erstes die Klasse Paddle
erstellt:
Paddle
# coding=utf-8
class Paddle(object):
def __init__(self):
self.w = 120
self.h = 15
self.pos = PVector(width/2 - self.w/2, height - 40)
self.isMovingLeft = False
self.isMovingRight = False
self.stepSize = 20
def display(self):
fill("#ff9664")
noStroke()
rect(self.pos.x, self.pos.y, self.w, self.h)
def update(self):
if self.isMovingLeft:
self.move(-self.stepSize)
elif self.isMovingRight:
self.move(self.stepSize)
def move(self, step):
self.pos.x += step
def checkEdges(self):
if self.pos.x <= 0:
self.pos.x = 0
elif self.pos.x + self.w >= width:
self.pos.x = width - self.w
Der Konstruktor legt die Größe des Paddles fest und initialisiert seine Position ungefähr in die Mitte des Fensters und 40 Pixel oberhalb des unteren Randes. Außerdem werden die boolschen Variabeln isMovingLeft
und isMovingRight
auf False
gesetzt. Die Funktionen display()
, update()
und checkEdges()
werden vom Hauptprogramm benötigt, move()
hingegen wird nur intern von update()
aufgerufen. display()
macht nichts anderes, als die Farbe für das Paddle festzulegen und an der aktuellen Position ein Rechteck zu zeichnen, update()
bewegt das Paddle je nach Zustand der boolschen Variablen isMovingRight
oder isMovinLeft
mit Hilfe der move()
-Funktion entweder nach rechts oder nach links. Sind beide boolschen Variablen False
, dann bewegt sich das Paddle eben nicht.
checkEdges()
sorgt dafür, daß das Paddle das Fenster nicht verläßt, sondern an der rechten oder linken Seite gestoppt wird.
Um das ganze in Action zu sehen, müssen wir das Hauptprogramm erweitern:
from gameworld import Paddle
def setup():
global paddle
size(605, 400)
paddle = Paddle()
def draw():
global paddle
background(0, 0, 0)
paddle.display()
paddle.checkEdges()
paddle.update()
def keyReleased():
global paddle
paddle.isMovingRight = False
paddle.isMovingLeft = False
def keyPressed():
global paddle
if key == CODED:
if keyCode == LEFT:
paddle.isMovingLeft = True
elif keyCode == RIGHT:
paddle.isMovingRight = True
Erst einmal wird natürlich die Klasse Paddle
importiert und in setup()
eine Instanz von Paddle erzeugt. Die draw()
-Schleife zeigt das Paddle erst einmal an, püft dann, ob es an eine Ecke stößt und führt anschließend die update()
-Funktion aus. Dazu müssen wir noch die Tasteneingaben abfragen. keyPressed()
setzt in Abhängigkeit davon, ob die rechte oder linke Pfeiltaste gedrückt wurde, entwederisMovingLeft
oder isMovingRight
auf True
. Sobald die Taste aber wieder losgelassen wird, setzt keyReleased
alles wieder auf False
.
Das kann jetzt getestet werden und das Paddle bewegt sich brav nach rechts oder links, wenn eine der Pfeltasten gedrückt wird.
Der (Bouncing) Ball
Als nächstes werde ich den Ball implementieren, erst einmal zu Testzwecken als Bouncing Ball, der von allen vier Seiten reflektiert wird. Die untere Seite werde ich später auskommentieren, damit der Ball ins Nirwana entschwinden kann, aber vorerst sieht die Klasse Ball
(in gamewolrd.py
) so aus:
class Ball(object):
def __init__(self):
self.r = 10
self.vel = PVector(1, 1)*4
self.dir = PVector(1, 1)
self.pos = PVector(width/2, height/2)
def update(self):
self.pos.x += self.vel.x*self.dir.x
self.pos.y += self.vel.y*self.dir.y
def display(self):
fill("#ffff64")
noStroke()
ellipse(self.pos.x, self.pos.y, self.r*2, self.r*2)
def checkEdges(self):
# rechter Rand
if (self.pos.x > width - self.r and self.dir.x > 0):
self.dir.x *= -1
# linker Rand
if (self.pos.x < self.r and self.dir.x < 0):
self.dir.x *= -1
# top
if (self.pos.y < self.r and self.dir.y < 0):
self.dir.y *= -1
# bottom (wird später gelöscht)
if (self.pos.y > height - self.r and self.dir.y > 0):
self.dir.y *= -1
Der Ball bekommt im Konstuktor einen Radius, eine Velocity und eine Richtung verpaßt. Diese werden jeweils als PVector
implementiert. Ebenfalls ein PVector
ist die Position, mit der der Ball erst einmal in die Mitte des Fenster plaziert wird. Wie aber die update()
-Methode zeigt, bewegt sich der Ball sofort und zwar erst einmal nach rechts unten. Sobald der Ball jedoch eine der Kanten des Fensters erreicht, bounced ihn checkEdges()
zurück. Die Methode update()
zeigt einfach nur den Ball in einer tennisballgelben Farbe an.
Das Hauptprogramm sieht jetzt erst einmal so aus:
from gameworld import Paddle, Ball
def setup():
global paddle, ball
size(605, 400)
paddle = Paddle()
ball = Ball()
def draw():
global paddle, ball
background(0, 0, 0)
paddle.display()
paddle.checkEdges()
paddle.update()
ball.display()
ball.checkEdges()
ball.update()
def keyReleased():
global paddle
paddle.isMovingRight = False
paddle.isMovingLeft = False
def keyPressed():
global paddle
if key == CODED:
if keyCode == LEFT:
paddle.isMovingLeft = True
elif keyCode == RIGHT:
paddle.isMovingRight = True
Der Ball bounced hin und her und ignoriert das Paddle geflissentlich. Vorerst bleibt das so, denn als nächstes möchte ich erst einmal die Bricks implementieren.
Das Spiel starten
Bevor ich aber die Steine implementiere, möchte ich erst einmal den ärgerlichen Umstand beheben, daß das Spiel erst dann auf die Tasteneingaben reagiert, wenn man mit der Maus in das Spielfenster klickt und diesem den Focus gibt. Also startet das Spiel auch erst, wenn man mit der Maustaste in das Fenster klickt, vorher passiert gar nichts. Dafür habe ich an den Anfang des Sketches mit
playingGame = False
eine boolsche Variable initialisiert und weiter unten die Funktion
def mousePressed():
global playingGame
playingGame = True
hinzugefügt. Die draw()
-Schleife wurde ebenfalls entsprechend geändert:
# Paddle
paddle.display()
if playingGame:
paddle.checkEdges()
paddle.update()
# Ball
if (ball.meets(paddle)):
if (ball.dir.y > 0):
ball.dir.y *= -1
ball.display()
if playingGame:
ball.checkEdges()
ball.update()
Eigentlich ist damit alles erledigt, nur leider ist das Spiel nun für Rechtshänder unspielbar geworden. Denn bevor man mit der rechten Hand von der Maus an die Pfeiltasten gekommen ist, ist der Ball schon lange im Aus. Daher habe ich eine alte Spieltechnik zusätzlich implementiert, Neben den Pfeiltasten steht a
für die Bewegung des Paddles nach links und d
für die Bewegung nach rechts. Dazu mußte die Funktion keyPressed()
geändert werden:
def keyPressed():
global paddle
if key == "a" or key == "A":
paddle.isMovingLeft = True
elif key == "d" or key == "D":
paddle.isMovingRight = True
if key == CODED:
if keyCode == LEFT:
paddle.isMovingLeft = True
elif keyCode == RIGHT:
paddle.isMovingRight = True
Nun kann man beim Start mit der rechten Hand die Maustaste drücken und mit der linken Hand schon über dem d
lauern, damit man sofort, wenn sich der Ball in Bewegung setzt, auch das Paddle nach rechts bewegen kann. Irgendwann im Verlauf des Spiels werden Rechtshänder vermutlich wieder zu den Pfeiltasten wechseln, aber Linkshänder sind mit der a
-d
-Kombination vielleicht generell glücklicher.
Nun soll natürlich der Ball, wenn er mit dem Paddle kollidiert, auch auf diesen reagieren. Dazu erhält er in der Klasse Ball
noch die Methode meets()
, die im obigen Code-Schnipsel auch schon aufgerufen wird:
def meets(self, paddle):
if (self.pos.y < paddle.pos.y and
self.pos.y > paddle.pos.y - self.r and
self.pos.x > paddle.pos.x - self.r and
self.pos.x < paddle.pos.x + paddle.w + self.r):
return True
else:
return False
Es ist eine boolsche Methode, die True
zurückliefert, wenn der Ball auf das Paddle trifft und andernfals False
. Und in der Hauptschleife kann man dann mit
if (ball.meets(paddle)):
if (ball.dir.y > 0):
ball.dir.y *= -1
einfach die y-Richtung umkehren, wenn meets()
True
zurückliefert. Zu Übungszwecken und um die Implementierung zu testen, bounced der Ball zu diesem Zeitpunkt immer noch auch vom Boden des Fensters zurück, dieses Verhalten möchte ich erst ganz zum Schluß ändern.
Jetzt aber: Die Klötzchen
Ich möchte drei Reihen von Klötzchen implementieren, die verschiedene »Leben« haben. Die unterste Reihe (grün) hat nur ein Leben und das Klötzchen verschwindet sofort, wenn es vom Ball getroffen wird. Die mittlere Reihe (rosa) hat zwei Leben, beim ersten Treffen des Balls verfärbt sich das Klötzchen grün und Anzahl der Leben wird um eines herabgesetzt. Analog hat die oberste Reihe (lila) drei Leben. Wird dort ein Klötzchen zum ersten Mal getroffen, wird es rosa, hat nur noch zwei Leben und wenn es dann noch einmal getroffen wird, wird es grün und besitzt nur noch ein Leben. Die Implementierung der Klasse Brick
beginnt daher so:
class Brick(object):
COLORS = {1: "#64ff96", 2: "#ff6496", 3: "#9664ff"}
def __init__(self, x, y, hits):
self.w = 75
self.h = 20
self.pos = PVector(x, y)
self.hits = hits
self.col = Brick.COLORS[hits]
def display(self):
fill(self.col)
stroke("#ffffff")
strokeWeight(2)
rect(self.pos.x, self.pos.y, self.w, self.h)
Der Konstruktor übernimmt drei Parameter, die x- und y-Position sowie die Anzahl der Hits. Die Methode display()
stellt jedes Klötzchen in seiner Farbe und an seiner Position dar. Der weiße, zwei Pixel starke Rand soll die einzelnen Klötzchen während der Entwicklung deutlich unterscheidbar machen.
Das Hauptprogramm bekommt nun folgende Ergänzungen. Ganz oben wird mit
from gameworld import Paddle, Ball, Brick
bricks = []
eine leere Liste bricks
initialisiert, die dann in setup()
gefüllt wird:
for x in range(5, width - 80, 75):
addBrick(x + 37.5, 50, 3)
addBrick(x + 37.5, 70, 2)
addBrick(x + 37.5, 90, 1)
dazu muß dem Programm auch noch die Funktion addBrick(x, y, hits)
hinzugefügt werden:
def addBrick(x, y, hits):
global brick, bricks
brick = Brick(x, y, hits)
bricks.append(brick)
Und die Darstellung in der draw()
-Schleife sieht dann so aus:
def draw():
global paddle, ball, bricks, playingGame
background(0, 0, 0)
# Bricks
for i in range(len(bricks)):
bricks[i].display()
Nun gewinnt man schon einen ungefähren Eindruck, wie das Spiel einmal aussehen soll. Man kann den Ball mit dem Paddle auffangen – mit ein wenig Geschick auch zu Beginn des Spieles, aber ansonsten bewegt der tennisgelbe Ball sich weiterhin wie ein Bouncing Ball. Als nächstes werden ich daher die Kollision mit den Klötzchen implementieren.