Beispielprojekte

Auto-Runner „Racing“

In diesem Beispielprojekt wird eine Implementierung eines einfachen Auto-Runners gezeigt.

_images/racing.JPG

Das fertige Spiel kann hier heruntergeladen werden.

Die Bühne

Zu Beginn wollen wir eine Fahrbahn als Bühne für das Spiel entwickeln. Dazu setzen wir zuerst den Hintergrund:

1
2
3
4
5
6
# width and height of the world
WIDTH = 800
HEIGHT = 768

def draw():
    screen.blit('road',(0,0))

Zusätzlich wollen wir eine Leitplanke auf der linken und rechten Seite realisieren. Dazu programmieren wir eine eigene Methode, die das erledigt:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# barriers
barriers = []

[...]

# initialises the barrier of the race track
def setupBarriers():
  for i in range(0, 6):
      barriers.append(Actor('barrier', (60, 64 + (i * 128))))
      barriers.append(Actor('barrier', (730, 64 + (i * 128))))

Diese Methode setzt auf beiden Seiten insgesamt sieben Stücke der Leitplanke. Die Methode muss in der Methode draw() aufgerufen werden. Jedes Stück der Leitplanke wird der Liste barriers hinzugefügt. Diese wird in der Methode draw() benötigt, um den Zugriff auf alle Stücke der Leitplanke zu vereinfachen und diese zu zeichnen:

def draw():
    screen.blit('road',(0,0))
    for b in barriers:
      b.draw()

Tipp

Du findest viele kostenlose Sprites, einschließlich diesem auf kenney.nl. Die Bilder für dieses Spiel stammen aus Racing pack.

Ein Auto (Sprite) erstellen und steuern

Nachdem die Fahrbahn fertig ist, soll ein Auto (Sprite) realisiert werden. Der folgende Code erstellt ein neues Auto und positioniert es mittig auf der Fahrbahn:

# the car
car = Actor('car_red')
car.pos = WIDTH/2, HEIGHT/2

In der Methode draw() muss zudem car.draw() aufgerufen werden. Um es so aussehen zu lassen, als ob das Auto sich bewegt, soll es in jedem Frame etwas nach unten verschoben werden:

# updates inbetween frames
def update():
  car.y += 1

Zur Steuerung des Autos mithilfe der Pfeiltasten der Tastatur definieren wir die Methode checkKeyboard(), die ebenfalls in update() aufgerufen werden muss:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# checks keyboard input
def checkKeyboard():
    # check for clicks on keyboard to move the car
    if keyboard.left:
        car.angle = +5
        car.x -= SPEED_CAR
    elif keyboard.right:
        car.angle = -5
        car.x += SPEED_CAR
    elif keyboard.up:
        car.y -= SPEED_CAR
    elif keyboard.down:
        car.y += SPEED_CAR
    else:
      car.angle = 0

Die (globale) Variable SPEED_CAR definiert, wieviel Pixel das Auto bei Klick einer Pfeiltaste verschoben wird. Zusätzlich wird durch die Änderung des Neigungswinkels des Autos (car.angle) das Auto in die entsprechende Richtung, in die es gesteuert wird, geneigt. Dieser Effekt kann auch vernachlässigt werden. Nun können wir das Auto auf der Fahrbahn steuern.

Ein erstes Hindernis: Kegel

Damit das Spiel etwas spannender wird, sollen dem Auto nun Hindernisse entgegen kommen, auf die unterschiedlich reagiert werden muss. Wir beginnen dabei mit einem Kegel, der vom Auto entweder nach links (wenn das Auto von rechts kommt) oder nach rechts (wenn das Auto von links kommt) verschoben werden soll.

_images/cone.png

Ähnlich wie der Leitplanke erstellen wir zuerst eine Liste der Hindernisse (hier: cones), die wir zur Speicherung der Kegel benötigen. Diese hilft uns später, alle Kegel zu aktualisieren (in der Methode draw):

1
2
3
4
5
6
7
8
9
# obstacles
cones = [] # when toching cones, they are pushed aside

# speeds and chances of obstacles
# speeds
SPEED_CONES = 3

# chances
CHANCE_CONES = 1

Das (zufällige) Erstellen sowie das Aktualisieren der Kegel bei Kollision mit dem Auto erledigt die Methode createAndUpdateCones(), die in der Methode update() aufgerufen werden muss:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# create a update cones
def createAndUpdateCones():
    # create new cone
    if(random.randint(0, 100) < CHANCE_CONES): # using a 1% chance
        x = random.randint(0, 400)
        cones.append(Actor('cone', (200 + x, 0)))
    # move the cones
    for c in cones:
        c.y += SPEED_CONES
        # check for collision of the car and cones
        if(car.colliderect(c)):
            if(car.x > c.x):
                c.x -= 100
            else:
                c.x += 100

Um den Befehl random.randint() verwenden zu können, muss zu Beginn des Programms der Befehl import random gesetzt werden. Das zufällige Erzeugen neuer Kegel wird von random.randint(0, 100) < CHANCE_CONES erledigt. Dieser Befehl gibt zurück, ob die erzeugte Zufallszahl kleiner als eine vorgegebene Wahrscheinlichkeit ist (CHANCE_CONES, hier: 1). Damit werden neue Kegel mit einer Wahrscheinlichkeit von 1 % pro Frame erstellt. Ähnlich wie das Auto, wird auch ein Kegel pro Frame um den Wert von SPEED_CONES nach unten verschoben. Die Kollision eines Kegels mit dem Auto wird mithilfe des Befehls car.colliderect(c) überprüft. Je nachdem ob das Auto von links oder rechts kommt, wird der Kegel nach rechts bzw. links verschoben. Somit ist das erste Hindernis, dem das Auto ausweichen muss, erstellt.

Weitere Hindernisse: Ölpfützen, Steine und Pfeile

Weitere Hindernisse, die dem Auto entgegen kommen und denen es ausweichen (bzw. angemessen reagieren) muss, können beispielsweise Ölpfützen, Steine und Pfeile sein.

_images/oil.png _images/rock.png _images/arrow.png

Der grundlegende Aufbau zur Programmierung von Hindernissen wurde bereits im vorigen Abschnitt bei den Kegeln gezeigt. Wir definieren für alle Hindernisse eine eigene Methode createAndUpdate<newObstacle>(), die das zufällige Erstellen und die Kollisionserkennung verarbeitet. Diese rufen wir in der Methode update() auf. Zudem müssen wir eine Liste, die wir für neue Hindernisse zur Speicherung der einzelnen Objekte verwenden, implementieren und diese in der Methode draw() benutzen. Die Unterschiede bei den Hindernissen liegt also großteils lediglich darin, wie sie auf Kollision mit dem Auto reagieren. Wir schlagen folgende Reaktionen vor:

  • Bei Kollision mit einer Ölpfütze dreht sich das Auto zufällig:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    [...]
    # check for collision of the car and oil
    if(car.colliderect(o)):
        spin()
    
    # makes the car spin
    def spin():
      car.y -= 10
      car.angle -= random.randint(-180, 180)
    
  • Bei Kollision mit einem Stein wird das Spiel beendet:

    # check for collision of the car and rocks
    if(car.colliderect(r)):
        # stop game
        gameover = "Game over! \n You hit a rock!"
    

    Der Text, der in der (globalen) Variable gameover gespeichert wird, soll also bei Kollision des Autos mit einem Stein angezeigt werden. Dieser wird mithilfe des folgenden Befehls in der Methode draw() gesetzt:

    screen.draw.text(gameover, (WIDTH/2 - 150, HEIGHT/2), color="white", fontsize=50, align="center")
    

    Zu Beginn des Spiels ist der Wert der Variable ein leerer String (gameover = ""), sodass dieser Befehl keine ersichtliche Wirkung hat und bei Kollision des Autos mit einem Stein auf einen nicht-leeren String gesetzt wird. Das Spiel wird hierbei jedoch nicht richtig beendet, sondern lediglich der Gameover-Text angezeigt. Das richtige Beenden des Spiels wird im folgenden Abschnitt umgesetzt.

  • Die Kollisionserkennung des Autos mit einem Pfeil ist hier als offene Aufgabe definiert. Beispielsweise könnte das Auto durch den Pfeil beschleunigt werden…

Countdown hinzufügen

Um das Ziel des Spiels (und somit auch das Spielende) sauber zu definieren, wird ein Countdown eingeführt. Der Spieler des Spiels muss den Hindernissen solange ausweichen, bis der Countdown abgelaufen ist (bzw. das Spiel durch Kollision mit einem Stein vorzeitig beendet wurde). Dazu werden die beiden neuen (globalen) Variablen running und countdown eingeführt. Die Variable gameover kennen wir bereits:

# running variable and game over and countdown text
running = True
gameover = ""
countdown = 60.0 (in seconds)

Der Countdown wird mittels des folgenden Befehls in der Methode draw() auf dem Bildschirm dargestellt:

screen.draw.text(str(countdown), (WIDTH/2 - 50, 25), color="white", fontsize=50, align="center")

Die folgende Methode aktualisert den Countdown (und stoppt das Spiel, falls er abgelaufen ist):

1
2
3
4
5
6
7
8
9
# reset the countdown each second
def resetCountdown():
    global countdown, gameover, running
    if(countdown > 0.0):
        countdown -= 1.0
    else:
        countdown = 0.0
        gameover = "Game over! \n You won!"
        running = False

Der Befehl running = False muss ebenfalls am Ende der Methode checkAndUpdateRocks() eingefügt werden, um bei der Kollisionserkennung entsprechend zu reagieren. Um die Methode resetCountdown() jede Sekunde (und nicht jedem Frame) aufzurufen, nutzen wir die Uhr von Pygame Zero.

Der folgende Code, der außerhalb der Methode resetCountdown() aufgerufen werden muss, ruft diese jede Sekunde auf:

# use the pgzero clock to call the resetCountdown() regulary each second
clock.schedule_interval(resetCountdown, 1.0)

Damit das Spiel nun nur läuft, wenn der Countdown noch aktiv ist und das Auto mit keinem Stein kollidiert ist, muss die Methode update() angepasst werden:

1
2
3
4
5
6
# updates inbetween frames
def update():
    if (running):
        # check for updates of the actor and obstacles
    else:
        clock.unschedule(resetCountdown)

So wird gewährleistet, dass das Spiel korrekt stoppt und der Countdown mittels des Befehls clock.unschedule(resetCountdown) nicht mehr aktualisiert wird. Somit haben wir ein kleines Auto-Runner-Spiel „Racing“ fertigstellt und können nur mit dem Spielen beginnen.

Mehr gefällig? Weitere Beispiele finden sich auf der englischen Seite von Pygame Zero.