image image


Space Invaders mit Python und PyGlet – Stage 2

Hatte ich in der ersten Folge dieser kleinen Tutorial-Reihe zur Spieleprogrammierung mit Python und PyGlet das Raketen-Spritesheet noch »fest verdrahtet« im Konstruktor der Klasse GameObject laden lassen, so ist spätestens mit der Aufnahme eines zweiten GameObjects klar, daß dies keine Lösung von Dauer ist. Außerdem ist das Laden von Bildern eine ressourcenfressende Angelegenheit, die man besser aus der eigentlichen Spielschleife herausnehmen und vorladen sollte. Zudem ist nicht jedes Bild ein Spritesheet, so zum Beispiel das Bild des Sternenhintergrundes, das in dieser Folge hinzugefügt werden soll.

image

Daher habe ich in einem ersten Schritt das Laden der Bilder aus der Klasse GameObjects herausgenommen und die Funktionen preload_image() und preload_image_animation() eingeführt:

def preload_image(image):
    img = pyglet.image.load("images/" + image)
    return img

def preload_image_animation(image, image_row, image_col, image_width, image_height):
    img = pyglet.image.load("images/" + image)
    img_seq = pyglet.image.ImageGrid(img, image_row, image_col, item_width = image_width, item_height = image_height)
    img_texture = pyglet.image.TextureGrid(img_seq)
    img_anim = pyglet.image.Animation.from_image_sequence(img_texture[:], 0.1, loop = True)
    return img_anim

Folgerichtig habe ich in der Klasse GameObject dann auch die (nun irreführende) Bezeichnung image durch sprite ersetzt, denn da ich diese Spielobjekte nun bewegen will, müssen sie, wie ich später noch zeigen werde, in PyGlet als Sprite-Objekte behandelt werden. Die Klasse GameObject sieht daher nun so aus:

class GameObject():
    
    def __init__(self, posx, posy, sprite = None):
        self.posx = posx
        self.posy = posy
        self.velx = 0
        self.vely = 0
        if sprite is not None:
            self.sprite = sprite
            self.sprite.x = self.posx
            self.sprite.y = self.posy
    
    def draw(self):
        self.sprite.draw()
    
    def update(self, dt):
        self.posx += self.velx*dt
        self.posy += self.vely*dt
        self.sprite.x = self.posx
        self.sprite.y = self.posy

Die Methoden draw() und update() sind neu hinzugekommen. draw() ist momentan noch nur ein Wrapper für sprite.draw() und da velx und vely im Konstruktor auf Null gesetzt wurden, macht update() erst einmal gar nichts. Aber das wird sich, wie Ihr Euch sicher vorstellen könnt, nun im Hauptprogramm ändern.

Als erste Änderung habe ich im Hauptprogramm eine Tastaturabfrage eingeführt, die zum einen die Rakete nach oben und unten bewegen und zum anderen das Programm beenden soll, wenn die Escape-Taste gedrückt wird. Aber zuerst einmal muß das Raumschiff ja geladen und dem Spielgeschehen hinzugefügt werden. Dafür muß ein wenig mehr importiert werden:

from pyglet.window import FPSDisplay, key
from pyglet.sprite import Sprite
from gameobjects import GameObject, preload_image, preload_image_animation

Für die Tastaturabfragen ist key hinzugekommen, wie oben erwähnt benötige ich nun auch die Klasse Sprite und aus gameobjects die neu geschaffenen Funktionen preload_image() und preload_image_animation(). Dann gilt es, ein paar Konstanten zu definieren:

PLAYERSPEED = 300
SPACESPEED = -50

Der Player ist nun ein Sprite und wird mit diesen beiden Zeilen ins Spiel geholt:

        player_spr = Sprite(preload_image_animation("Spritesheet_64x29.png", 4, 1, 64, 29))
        self.player = GameObject(50, 300, player_spr)

Und nun die angekündigten Tastaturabfragen: Wird die Pfeiltasten nach oben oder die Pfeiltaste nach unten gefdrückt, wird die Velocity (vely) entsprechend auf +/-PLAYERSPEED, werden die Tasten wieder losgelassen, wird sie wieder auf 0 gesetzt.

    def on_key_press(self, symbol, modifiers):
        if symbol == key.UP:
            self.player.vely = PLAYERSPEED
        if symbol == key.DOWN:
            self.player.vely = -PLAYERSPEED
    
    def on_key_release(self, symbol, modifiers):
        if symbol in (key.UP, key.DOWN):
            self.player.vely = 0

Nun könnt Ihr die Rakete nach oben und nach unten bewegen, aber damit der Eindruck einer Vorwärtsbewegung entsteht, möchte ich noch einen Sternenhimmel konstruieren, der langsam nach links weggleitet und so zumindest die Illusion erzeugt, daß sich die Rakete auch vorwärts durch deb Weltraum bewegt.

Auch das Bild des Sternenhimmels habe ich dem freien Set (CC-BY-3.0) von Jacob Zinman-Jeanes entnommen. Es ist 600x1782 Pixel groß und der Zeichner hat sich große Mühe gegeben, daß die Ränder rechts und links einen kontinuierlichen Übergang erzeugen. Wir laden daher das Bild zwei mal und setzen das zweite Bild immer rechts an, wenn das erste Bild aufhört. Dazu mußte ich erst einmal eine Liste mit den Bildern erzeugen und auch diese als Sprites initialisieren:

        self.space_list = []
        self.space_img = preload_image("farback.gif")
        for i in range(2):
            self.space_list.append(GameObject(i*1782, 0, Sprite(self.space_img)))
        
        for space in self.space_list:
            space.velx = SPACESPEED

Durch die Multiplikation mit der Laufvariablen »i« wird das erste Bild auf die x-Position 0 und das zweite Bild auf die x-Position 1782 gesetzt.

Wie Ihr seht, habe ich dem space auch gleich eine Geschwindigkeit in der x_Richtung mitgegeben. Diese als Konstante vorzudeklarieren macht Sinn, da ich zu Testzwecken die Geschwindigkeit auf -200 heraufgesetzt hatte, damit der Himmel schneller durchlief und ich so auch schneller erkennen konnte, ob das mit dem Ankleben der Ränder auch funktionierte. Da die SPACESPEED auch noch an anderer Stelle vorkommt, brauchte ich bloß den Wert der Konstante verändern und konnte so sicher sein, daß nach dem Zurücksetzen alles wieder in Ordnung war.

Zum Bewegen des Sternenhimmels habe ich die Funktion update_space() eingeführt:

    def update_space(self, dt):
        for space in self.space_list:
            space.update(dt)
            if space.posx <= -1882:
                self.space_list.remove(space)
                self.space_list.append(GameObject(1682, 0, Sprite(self.space_img)))
            space.velx = SPACESPEED

Wenn das »erste« Bild die Position -1882 erreicht hat, wird es aus der Liste entfernt und hinten an der Position 1682 wieder angehängt. Dieser Versatz von 100 Pixeln brachte visuell das beste Ergebnis. Dennoch, wer genau hinschaut kann beim Übergang die Schnittkanten feststellen. Aber dafür muß man schon sehr genau hinschauen, und wer tut das im Geschehen und der Hektik eines Ballerspiels? Jetzt noch die Methoden on_draw() und update() aktualisieren

    def on_draw(self):
        self.clear()
        for space in self.space_list:
            space.draw()
        self.player.draw()
        
        self.fps_display.draw()

    def update(self, dt):
        self.player.update(dt)
        self.update_space(dt)

und die Illusion eines gemächlich durch das All gleitenden Raumschiffes – wie es auch der obige Screenshot andeutet – ist perfekt.

Das Vorladen der Bilder hatte übrigens die Konsequenz, daß die Framerate nun kontinuierlich auf 60 FPS konstant blieb.

Zur Übersicht und damit Ihr das alles auch nachprogramieren könnt, hier nun noch der komplette Quellcode, der im Vergleich zu Stage 1 schon bedeutend umfangreicher geworden ist. Zuerst die Datei gameobjects.py:

import pyglet
from pyglet.sprite import Sprite

def preload_image(image):
    img = pyglet.image.load("images/" + image)
    return img

def preload_image_animation(image, image_row, image_col, image_width, image_height):
    img = pyglet.image.load("images/" + image)
    img_seq = pyglet.image.ImageGrid(img, image_row, image_col, item_width = image_width, item_height = image_height)
    img_texture = pyglet.image.TextureGrid(img_seq)
    img_anim = pyglet.image.Animation.from_image_sequence(img_texture[:], 0.1, loop = True)
    return img_anim

class GameObject():
    
    def __init__(self, posx, posy, sprite = None):
        self.posx = posx
        self.posy = posy
        self.velx = 0
        self.vely = 0
        if sprite is not None:
            self.sprite = sprite
            self.sprite.x = self.posx
            self.sprite.y = self.posy
    
    def draw(self):
        self.sprite.draw()
    
    def update(self, dt):
        self.posx += self.velx*dt
        self.posy += self.vely*dt
        self.sprite.x = self.posx
        self.sprite.y = self.posy

Dann das eigentliche Hauptprogramm, das ich in die Datei stage02.py gepackt habe:

import pyglet
from pyglet.window import FPSDisplay, key
from pyglet.sprite import Sprite
from gameobjects import GameObject, preload_image, preload_image_animation
import os

TITLE = "Space Invaders Stage 2"
PLAYERSPEED = 300
SPACESPEED = -50

class GameWindow(pyglet.window.Window):
    
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.set_location(100, 100)
        self.frame_rate = 1/60.0
        self.fps_display = FPSDisplay(self)
        self.fps_display.label.font_size = 24
        
        # Hier wird der Pfad zum Verzeichnis des ».py«-Files gesetzt
        # Erspart einem das Herumgehample in TextMate mit dem os.getcwd()
        # und os.path.join()
        file_path = os.path.dirname(os.path.abspath(__file__))
        os.chdir(file_path)
        
        self.space_list = []
        self.space_img = preload_image("farback.gif")
        for i in range(2):
            self.space_list.append(GameObject(i*1782, 0, Sprite(self.space_img)))
        
        for space in self.space_list:
            space.velx = SPACESPEED
        
        player_spr = Sprite(preload_image_animation("Spritesheet_64x29.png", 4, 1, 64, 29))
        self.player = GameObject(50, 300, player_spr)
    
    def on_key_press(self, symbol, modifiers):
        if symbol == key.UP:
            self.player.vely = PLAYERSPEED
        if symbol == key.DOWN:
            self.player.vely = -PLAYERSPEED
    
    def on_key_release(self, symbol, modifiers):
        if symbol in (key.UP, key.DOWN):
            self.player.vely = 0
    
    def on_draw(self):
        self.clear()
        for space in self.space_list:
            space.draw()
        self.player.draw()
        
        self.fps_display.draw()
    
    def update_space(self, dt):
        for space in self.space_list:
            space.update(dt)
            if space.posx <= -1882:
                self.space_list.remove(space)
                self.space_list.append(GameObject(1682, 0, Sprite(self.space_img)))
            space.velx = SPACESPEED
    
    def update(self, dt):
        self.player.update(dt)
        self.update_space(dt)

win = GameWindow(900, 600, TITLE, resizable = False)
pyglet.clock.schedule_interval(win.update, win.frame_rate)
pyglet.app.run()

Den gesamten Quellcode samt sämtlicher Assets gibt es natürlich auch im GitHub-Repositorium zu dieser kleinen Tutorial-Reihe.

Ein Spiel ohne Gegner ist langweilig, daher möchte ich als nächstes diese in das Spiel einbauen und dem Player eine Laserkanone spendieren, damit er sich wehren kann. Wie genau, weiß ich noch nicht, aber ich denke, mir wird da schon etwas einfallen. 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, 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


Werbung


image  image  image
image  image  image


image