image image


Space Invaders mit Python und PyGlet: UFOs, wir wollen UFOs

In diesem dritten Teil meiner kleinen Tutorial-Reihe zur Spieleprogrammierung mit Python und PyGlet (hier Teil 1 und Teil 2) möchte ich dem kleinen Raumschiff die entsprechenden Gegner spendieren. Dazu habe ich aus dem freien Set (CC-BY-3.0) von Jacob Zinman-Jeanes fünf Spritesheets mit farbigen Ufos entnommen, die zitternd über den Bildschirm schweben und sich bedrohlich auf das Raumschiff zubewegen sollen. Im Endeffekt wird es so aussehen, wie dieser Screenshot (und glaubt mir oder programmiert es nach: Sowohl die UFOs wie auch die Rakete sind animiert):

image

Als erstes stand mal wieder ein Refactoring an: Ich hielt es für angebracht, alle Spielobjekte, das heißt sowohl den Sternenhintergrund, die UFOs wie auch den Spieler als Unterklassen der Klasse GameObject zu implementieren. Die entsprechende Datei habe ich gameobjects2.py genannt und die Unterklassen hinzugefügt:

class Player(GameObject):
    
    def __init__(self, posx, posy, sprite = None):
        super().__init__(posx, posy, sprite)

    def update(self, dt):
        self.posx += self.velx*dt
        self.posy += self.vely*dt
        if self.posy >= HEIGHT - 40:
            self.posy = HEIGHT - 40
        if self.posy <= 10:
            self.posy = 10
        self.sprite.x = self.posx
        self.sprite.y = self.posy

class Ufo(GameObject):
    
    def __init__(self, posx, posy, sprite = None):
        super().__init__(posx, posy, sprite)
        self.speed = -100

class Space(GameObject):
    
    def __init__(self, posx, posy, sprite = None):
        super().__init__(posx, posy, sprite)

Die (neue Klasse) Ufo wie auch die Klasse Space unterscheiden sich (bis auf die UFO-Geschwindigkeit) bisher noch nicht groß von der Klasse GameObject – das wird sich aber in einer der nächsten Folgen zumindest für die UFOs noch ändern.

Neu an der Klasse Player ist, daß dieser nun ein Randabfrage bekommen hat. Die Rakete kann nun das Spielfenster nicht mehr verlassen. Die Randabfrage für die UFOs habe ich – da diese in einer Liste erolgt – im Hauptprogramm belassen. In diesem wurden als erstes die entsprechenden import-Statements angepaßt:

import pyglet
from pyglet.window import FPSDisplay, key
from pyglet.sprite import Sprite
from gameobjects2 import GameObject, Player, Ufo, Space, preload_image, preload_image_animation
import os

Ähnlich dem Hintergrund werden nun die Bilder der UFOs in einer Liste vorgeladen und die Objekte selber dann ein einer Schleife initialisiert:

        # Gegner
        self.ufo_list = []
        self.ufo_img1 = preload_image_animation("eSpritesheet_40x30.png", 6, 1, 40, 30)
        self.ufo_img2 = preload_image_animation("eSpritesheet_40x30_hue1.png", 6, 1, 40, 30)
        self.ufo_img3 = preload_image_animation("eSpritesheet_40x30_hue4.png", 6, 1, 40, 30)
        self.ufo_img4 = preload_image_animation("eSpritesheet_40x30_hue2.png", 6, 1, 40, 30)
        self.ufo_img5 = preload_image_animation("eSpritesheet_40x30_hue3.png", 6, 1, 40, 30)
        for i in range(10):
            self.ufo_list.append(Ufo(840, 540 - i*40, Sprite(self.ufo_img1)))
            self.ufo_list.append(Ufo(760, 540 - i*40, Sprite(self.ufo_img2)))
            self.ufo_list.append(Ufo(680, 540 - i*40, Sprite(self.ufo_img3)))
            self.ufo_list.append(Ufo(600, 540 - i*40, Sprite(self.ufo_img4)))
            self.ufo_list.append(Ufo(520, 540 - i*40, Sprite(self.ufo_img5)))

Das Vorladen der UFO-Bilder hätte bei entsprechender Umbenennung der Dateinamen natürlich auch in einer Schleife geschehen können, aber ich hatte ursprünglich nur zwei Spritesheets vorgesehen. Doch als ich dann sah, wie smooth die Animationen abliefen, habe ich die anderen drei Spritesheets einfach per copy and paste dazugeladen.

Die on_draw()-Funktion mußte natürlich um die Zeilen ergänzt werden, die die UFOs zeichnen sollen. Daher sind diese beiden Zeilen hinzugekommen:

        for ufo in self.ufo_list:
            ufo.draw()

Für das Update der UFOs habe ich eine eigene Funktion geschrieben:

    def update_ufo(self, dt):
        for ufo in self.ufo_list:
            ufo.update(dt)
            if ufo.posy <= 10:
                ufo.posy = 10
                ufo.posx -= 40
                ufo.speed *= -1
            if ufo.posy >= HEIGHT - 50:
                ufo.posy = HEIGHT - 50
                ufo.posx -= 40
                ufo.speed *= -1
            ufo.vely = ufo.speed

Die Ränderbehandlung scheint ein wenig seltsam. Ziel war es jedoch – analog zu dem originalen Space Invaders – die UFOs um eine Reihe nach vorne rücken zu lassen, sobald sie den Fensterrand erreicht haben. Darum auch der Abstand von 80 Pixeln je UFO-Reihe. Da die UFOs jeweils 30 Pixel breit sind, überlappen sie sich dann nicht beim Vorrücken auf den Spieler.

Nach all diesen Vorbereitungen mußte die update()-Funktion nur ein zusätzliche Zeile spendiert bekommen, damit alles wie gewünscht läuft. Sie sieht nun so aus:

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

Wie gewohnt nun das vollständige Programmlisting, damit Ihr das bisherige Vorgehen auch nachempfinden und -programmieren könnt. Zuerst die Datei gameobjects2.py:

import pyglet
from pyglet.sprite import Sprite

WIDTH = 900
HEIGHT = 600

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

class Player(GameObject):
    
    def __init__(self, posx, posy, sprite = None):
        super().__init__(posx, posy, sprite)

    def update(self, dt):
        self.posx += self.velx*dt
        self.posy += self.vely*dt
        if self.posy >= HEIGHT - 40:
            self.posy = HEIGHT - 40
        if self.posy <= 10:
            self.posy = 10
        self.sprite.x = self.posx
        self.sprite.y = self.posy

class Ufo(GameObject):
    
    def __init__(self, posx, posy, sprite = None):
        super().__init__(posx, posy, sprite)
        self.speed = -100

class Space(GameObject):
    
    def __init__(self, posx, posy, sprite = None):
        super().__init__(posx, posy, sprite)

Und dann das Hauptprogramm, das sich in der Datei stage03.py befindet:

import pyglet
from pyglet.window import FPSDisplay, key
from pyglet.sprite import Sprite
from gameobjects2 import GameObject, Player, Ufo, Space, preload_image, preload_image_animation
import os

TITLE = "Space Invaders Stage 3"
PLAYERSPEED = 300
SPACESPEED = -50
WIDTH = 900
HEIGHT = 600

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)
        
        # Hintergrund
        self.space_list = []
        self.space_img = preload_image("farback.gif")
        for i in range(2):
            self.space_list.append(Space(i*1782, 0, Sprite(self.space_img)))
        
        for space in self.space_list:
            space.velx = SPACESPEED
        
        # Gegner
        self.ufo_list = []
        self.ufo_img1 = preload_image_animation("eSpritesheet_40x30.png", 6, 1, 40, 30)
        self.ufo_img2 = preload_image_animation("eSpritesheet_40x30_hue1.png", 6, 1, 40, 30)
        self.ufo_img3 = preload_image_animation("eSpritesheet_40x30_hue4.png", 6, 1, 40, 30)
        self.ufo_img4 = preload_image_animation("eSpritesheet_40x30_hue2.png", 6, 1, 40, 30)
        self.ufo_img5 = preload_image_animation("eSpritesheet_40x30_hue3.png", 6, 1, 40, 30)
        for i in range(10):
            self.ufo_list.append(Ufo(840, 540 - i*40, Sprite(self.ufo_img1)))
            self.ufo_list.append(Ufo(760, 540 - i*40, Sprite(self.ufo_img2)))
            self.ufo_list.append(Ufo(680, 540 - i*40, Sprite(self.ufo_img3)))
            self.ufo_list.append(Ufo(600, 540 - i*40, Sprite(self.ufo_img4)))
            self.ufo_list.append(Ufo(520, 540 - i*40, Sprite(self.ufo_img5)))
        
        player_spr = Sprite(preload_image_animation("Spritesheet_64x29.png", 4, 1, 64, 29))
        self.player = Player(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
        if symbol == key.ESCAPE:
            pyglet.app.exit()
    
    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()
        for ufo in self.ufo_list:
            ufo.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_ufo(self, dt):
        for ufo in self.ufo_list:
            ufo.update(dt)
            if ufo.posy <= 10:
                ufo.posy = 10
                ufo.posx -= 40
                ufo.speed *= -1
            if ufo.posy >= HEIGHT - 50:
                ufo.posy = HEIGHT - 50
                ufo.posx -= 40
                ufo.speed *= -1
            ufo.vely = ufo.speed
                
    
    def update(self, dt):
        self.player.update(dt)
        self.update_ufo(dt)
        self.update_space(dt)

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

Ich habe dieser Tutorial-Reihe ein GitHub-Repositorium spendiert, in dem Ihr nicht nur den Quellcode, sondern auch alle bisher verwendeten Assets finden könnt. Doch denkt immer daran: Man lernt mehr, wenn man den Quellcode nicht einfach kopiert, sondern ihn Zeile für Zeile abschreibt.

Für die nächste Folge plane ich, für den Spieler eine Laserrakete zu implementieren, damit er sich gegen die herannahenden UFOs auch verteidigen kann. Dafür muß dann natürlich auch eine Kollisionserkennung implementiert werden. 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