Objektorientierte Spieleentwicklung mit Python und PyGame


boeseronkel2k

Active Member
Staff member
Joined
Oct 9, 2005
Messages
985
Hallo zusammen,
da das Python Unterforum sehr am schlafen ist, habe ich mir Überlegt ein kleines Tutorial zur Spieleentwicklung in Python zu Schreiben. Dieses Tutorial setzt entweder Grundkenntnisse in Python voraus, oder aber ihr müsst die Python Funktionen nachlesen, ich versuche es so simpel wie Möglich zu halten, und sollten Fragen aufkommen, einfach stellen.

Fangen wir an mit einem kleinen Spiel, welches ich als erstes auf dem C=64 entwickelt habe.
Es ist relativ simpel, und zwar gibt es ein rechteckiges Spielfeld. In dieses Spielfeld können Kreise gesetzt werden. Diese Kreise haben eine Richtung welche beschreibt wie sich die Position verändern soll beim Nächsten Spielzustand.

Warum habe ich dieses Spiel in Python anstatt in einer anderen Sprache wie C oder C++ umzusetzen? Ganz einfach Python ist eine einfach zu erlernende Sprache, und mit PyGame gibt es einen Wrapper um die SDL mit Python anzusteuern, also warum sich mit Kleinigkeiten quälen, wenn Python uns diese abnehmen kann.

Nun gibt es bestimmt noch einige welche sich Fragen: Warum OOP (Objektorientierte Programmierung). Auch dieses ist leicht zu beantworten, weil es einem viel Arbeit abnehmen kann. In diesem Beispiel werdet ihr Feststellen dass es für die Spiellogik einfach ist wenn die einzelnen Objekte selbst wissen ob sie mit einem anderen Objekt kollidieren.

Wir fangen einfach mal mit einer Datei welche die Spieloptionen beschreibt an:
Wir nennen sie „settings.py“. In dieser Datei erstellen wir Variablen welche die Spielfeldeckpunkte beschreibt und ähnliches.

Hier der Inhalt, sollte selbsterklärend sein
Code:
<i>
</i>#Die Position der Pfeile fuer die naechste Kreisrichtung
ArrowPosition = (100, 440)
#Die Position fuer die Punkte
PointsPosition = (400, 440)
#Der Versatz des Spielfeldes
Offset = (74, 63)
#Die Groesse des Spielfeldes
Field = (721-Offset[0],384-Offset[1])
#Die Groesse des Bildschirmes (Pandora ;)
Size = (800, 480)
(Falls nicht, (x,y) ist ein Tupel, ein einfaches Array welches beliebig viele Werte halten kann)

So dann ein paar Informationen über meine gedachten Klassen:

Eine Klasse CircleFun: Dient für das Eigentliche Spiel
Eine Klasse Circle: Eine Klasse die einen Kreis definiert
Eine Klasse Direction: [Eine Abstrakte Klasse]Beschreibt die Bewegung eines Kreises
Eine Klasse DirectionUp: Implementiert „Direction“ Klasse für die Bewegungen nach Oben
Eine Klasse DirectionDown: Implementiert „Direction“ Klasse für die Bewegung nach Unten
Eine Klasse DirectionLeft: Na ratet mal ;)
Eine Klasse DirectionRight: ...

Soderle, dann Fangen wir von Unten herum an das Spiel zu Implementieren:

Zuerst schreiben wir in die „circlefun.py" ersteinmal die benötigten Importe hinein:
Code:
import pygame
import os
import sys

from random import choice
from settings import *
import os
import pygame

from math import sqrt

Und nun fangen wir einmal an mit der abstrakten Richtungsklasse an.

Diese Klasse (und die nachfolgenden) implementieren wir in der Hauptdatei „circlefun.py“.
Für die Klasse implementieren wir einen Konstruktor (diese spezielle Funktion wird ausgeführt wenn von dieser Klasse eine Instanz erstellt wird). Eine Funktion „draw“ welche das Icon der Klasse malt, eine (abstrakte) Funktion „move“ welche bei den später abgeleiteten Klassen einfach überschrieben wird und die neue Position des Kreises berechnet. Und dann nehmen wir noch eine statische Funktion random dazu, welches uns eine zufällig ausgewählte von dieser Klasse abgeleitete Klasse zurückliefert. Statisch heißt um diese Funktion auszuführen benötigen wir keine Instanz der Klasse.
Code:
""" Dies ist die Basisklasse welche eine Bewegungsrichtung fuer einen Kreis beschreibt """
class Direction:
    """ Der Konstruktor """
    def __init__(self, imagesize):
        self.imagesize = imagesize
    
    """ Diese Funktion gibt das Bild der Position aus (benoetigt fuer die Anzeige des naechsten Kreises """
    def draw(self, dstImage):
        if self.image:
            dstImage.blit(self.image, ArrowPosition)

    """ Diese Funktion gibt die neue Position des kreises zurueck und wird von den eigentlichen Unterklassen ueberschrieben"""
    def move(self, oldpos):
        return oldpos

    """ Diese Funktion dient dazu eine Zufaellige Directionsklasse auszuwaehlen, @staticmethod bedeutet, dass hierfuer keine Klasseninstanz benoetigt wird"""
    @staticmethod
    def random(imagesize):
        direction = choice((DirectionUp, DirectionDown, DirectionLeft, DirectionRight))
        return direction(imagesize)

Nun haben wir die Hauptrichtungsklasse erstellt, nun erstellen wir hiervon abgeleitet Klassen, welche die „move“ Methode dementsprechend überschreiben. Dadurch haben wir sichergestellt, dass wenn wir Beispielsweise einen Bug bei der Positionsberechnung in der move Funktion für die sich nach unten bewegende Kreise fixen, werden wir höchstwahrscheinlich keine Bugs in die anderen Richtungsklassen mit einbauen .

Nun direkt unter die „Direction“ Klasse implementieren wir die abgeleitete Klassen für die jeweiligen Richtungen.
Code:
""" Fuer einen Kreis, welcher sich immer nach oben bewegt"""
class DirectionUp(Direction):
    image = pygame.image.load(os.path.join(os.path.dirname(__file__),  "data", "ArrowUp.png"))
    
    """ Anpassung der Kreisposition """
    def move(self, oldpos):
        y = oldpos[1]
        if y > 0:
            y -= 1
        else:
            y = Field[1] - self.imagesize.height
        return (oldpos[0], y)

""" Fuer einen Kreis, welcher sich immer nach Unten bewegt"""
class DirectionDown(Direction):
    image = pygame.image.load(os.path.join(os.path.dirname(__file__),  "data", "ArrowDown.png"))

    """ Anpassung der Kreisposition """
    def move(self, oldpos):
        y = oldpos[1]
        if y < Field[1] - self.imagesize.height:
            y += 1
        else:
            y = 0
        return (oldpos[0], y)
        
""" Fuer einen Kreis welcher sich immer nach links bewegt"""
class DirectionLeft(Direction):
    image = pygame.image.load(os.path.join(os.path.dirname(__file__),  "data", "ArrowLeft.png"))

    """ Anpassung der Kreisposition """
    def move(self, oldpos):
        x = oldpos[0]

        if x > 0:
            x -= 1
        else:
            x = Field[0] - self.imagesize.width
        return (x, oldpos[1])

""" Fuer einen Kreis welcher sich immer nach rechts bewegt"""
class DirectionRight(Direction):
    image = pygame.image.load(os.path.join(os.path.dirname(__file__),  "data", "ArrowRight.png"))
    
    """ Anpassung der Kreisposition """
    def move(self, oldpos):
        x = oldpos[0]

        if x < Field[0] - self.imagesize.width:
            x += 1
        else:
            x = 0
        return (x, oldpos[1])

Nachdem wir schon sooooo viele Klassen implementiert haben fehlen nur noch 2., eine für die Kreise und einen für das eigentliche Spiel.

Nun zu den Kreisen:
Ein Kreis braucht eine Position und eine Richtung, diese legen wir wieder mit dem Konstruktor fest. Dazu ist auch eine Helferfunktion gut, welche aus dem Kreisbild sich den Kreismittelpunkt und den Radius berechnet. Dann bekommt der Kreis noch die Methode „move“ welche die aktuelle Position anpasst durch die jeweilige Directions Klasse. Und zu guter letzt brauchen wir dafür noch eine Funktion welche guckt ob der Kreis mit einem anderen Kollidiert.

Implementiert könnte das Ganze so ausschauen:

Code:
""" 
Die Klasse Circle beschreibt einen Kreis
Und wie dieser sich verhaelt
"""
class Circle:
    image = pygame.image.load(os.path.join(os.path.dirname(__file__),  "data", "circle.png"))

    def __init__(self, pos=(0.0,0.0), direction=Direction.random(None)):
        #Der Kreis hat eine Position an der er sich Befindet
        self.pos = pos
	#und eine Richtung in die er sich bewegt
        self.direction = direction

    """ Die Methode draw dient dazu den Kreis auf einem als Parameter uebergebenen Surface zu malen"""
    def draw(self, dstImg):
        if self.image:
            imgPos = self.image.get_rect()
            dstImg.blit(self.image, (Offset[0] + self.pos[0], Offset[1] + self.pos[1]))

    """ Die methode dient dazu aus einem Kreis den Mittelpunkt 
    und den Radius zu bestimmen, dies ist fuer die Kollisionserkennung noetig"""
    def calculate_center_and_r(self):
        center_x = self.pos[0] + (self.image.get_rect().width / 2.0)
        center_y = self.pos[1] + (self.image.get_rect().width / 2.0)
        r = (self.image.get_rect().width + self.image.get_rect().width) / 4.0
        return (center_x, center_y, r)
    
    """ Die Methode Move bewegt den Kreis, die Richtung ist durch die Instanz der "Direction" Klasse bestimmt """
    def move(self):
        self.pos = self.direction.move(self.pos)

    """ Diese funktion gibt zurueck ob dieser Kreis mit dem ihm uebergebenen Kreis kollidiert """
    def colides(self, other):
        #Berechne die Mittelpunkte und die Radien der beiden Kreise
        center_x, center_y, r = self.calculate_center_and_r()
        other_x, other_y, other_r = other.calculate_center_and_r()
        distX = center_x - other_x
        distY = center_y - other_y

        #Berechne mit dem Satz des Pythagoras die Distanz der Kreise
        dist = sqrt((distX * distX) + (distY * distY))
        
        #Wenn die Distanz kleiner oder gleich der Entfernung der Kreise ist sind sie kollidiert
        return dist <= (r + other_r)

Nun sind wir fast am Ziel nun brauchen wir noch die Hauptklasse, welche sich um Mausevents kümmert, die einzelnen Kreise sich malen lässt, den Punktestand hochzählt und diese üblichen Spielfunktionen implementiert.
Code:
""" Die eigentliche Spielklasse """
class CircleFun:

    """ Der Konstruktor der Klasse, welcher ggf. auch das Fenster oeffnet """
    def __init__(self, init_screen=True):
        #setz die Punkte auf null
        self.points = 0
        #das Spiel laeuft
        self.running = True
        #initialisiere eine Clock fuer die Frameratenbegrenzung (und ggf. FPS Anzeige)
        self.clock = pygame.time.Clock()
        #lade die Spieldaten
        self.load_data()
        #erstelle eine Schriftart zum Anzeigen der Punkte
        self.font = pygame.font.Font(None, 17)
        #initialisiere ggf. das Fenster
        if init_screen:
            self.brk = False
            self.screen = pygame.display.set_mode(Size)
        #erstell ein array fuer die Kreise
        self.circles = []
        #setze die naechste Richtung fuer einen Kreis
        self.next_direction = Direction.random(Circle.image.get_rect())
    
    """ Eine Funktion zum Laden der Daten """
    def load_data(self):
        #hole das Hintergrundbild aus dem Data ordner mit dem namen main.png
        self.background = pygame.image.load(os.path.join("data", "main.png"))
    
    """ Die Funktion wenn ein neuer Kreis hinzugefuegt werden soll """
    def append_circle(self, pos):
        
        #hole die Position des Kreises (Mausklick Position - Versatz Position)
        xpos = pos[0] - Offset[0]
        ypos = pos[1] - Offset[1]
        
        #Erstelle einen Kreis mit den Positionen und der naechsten Richtung
        c = Circle((xpos, ypos), self.next_direction)
        
        #Die Position muss Positiv sein und im Feld liegen, ausserdem darf der Kreis mit keinem anderen kollidieren
        if xpos >= 0 and xpos < Field[0] - c.image.get_rect().width and \
                ypos >= 0 and ypos < Field[1] - c.image.get_rect().height \
                and (not any([x.colides(c) for x in self.circles])):
            #wenn ja fuege diesen Kreis dem array hinzu
            self.circles.append(c)
            #erhoehe den Punktestand
            self.points += 1
            #und setze die naechste Richtung
            self.next_direction = Direction.random(Circle.image.get_rect())

    """ Das Event callback, hier werden die Mausclicks abgefangen und auf das Beenden reagiert """
    def handle_events(self):
        for event in pygame.event.get():

            #wurde das fenster geschlossen?
            if event.type == pygame.QUIT:
                #dann beende das Programm apprupt
                sys.exit()

            elif event.type == pygame.KEYDOWN: 
                if event.key == pygame.K_ESCAPE:
                    #bei escape gedrueckt, beende es sanft
                    self.running = False

            elif event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
                #linke Maustaste gedrueckt, dann fuege einen Kreis mit der momentaten Position hinzu
                self.append_circle(event.pos)

    """ Die Funktion welche sich um die gesammte darstellung kuemmert """
    def draw(self):
        #male das Hintergrundbild
        self.screen.blit(self.background, self.background.get_rect())
        #male das "Naechste Richtung" Symbol
        self.next_direction.draw(self.screen)
        #Schreibe den Punktestand
        self.draw_points()
        #gehe jeden Kreis durch
        for circle in self.circles:
            #und lass diesen sich selber auf das Bild malen
            circle.draw(self.screen)
        #Schreibe die Bilddaten direkt in den Buffer
        pygame.display.flip()
        #Limitiere die FPS auf 50
        self.clock.tick(50)

    """ Die Move Funktion kuemmert sich um die Bewegungen im Spiel """
    def move(self):
        #durchlaufe jeden Kreis
        for circle in self.circles:
            #lass jeden Kreis sich selbst in seine Richtung bewegen
            circle.move()

    """ Ueberpruefe ob Kreise kollidiert sind """
    def check(self):
        #durchlaufe jeden Kreis
        for circle in self.circles:
            #durchlaufe jeden Kreis
            for otherCircle in self.circles:
                #gucke ob der kreis nicht der eigene Kreis ist,
                #und auch nicht mit anderen kollidiert
                if circle != otherCircle and circle.colides(otherCircle):
                    #ansonnsten gehe zur Funktion 'end' dieser Klasse
                    self.end()

    """ Kreise sind Kollidiert, was tun? """
    def end(self):
        #Man koennte einen Statusfenster aufmachen oder dergleichen


        #Oder einfach das Spiel neustarten:
        self.__init__(False)

        #Oder da dies Geschicklichkeitsspiel so doof ist einfach das Spiel beenden:
        #self.running = False

    """ Die Hauptschleife """
    def mainloop(self):
        #wird durchlaufen so lange das Spiel laeuft
        while self.running:
            #bearbeite die events (Maus / Tastatureingaben / Beenden)
            self.handle_events()
            #bewege die Kreise
            self.move()
            #pruefe auf Kollisionen
            self.check()
            #und dann Male alles
            self.draw()

    """ Eine kleine Funktion zum malen des Punktestands """
    def draw_points(self):
        #Mache den String 'x Punkte' zu einem Bild
        text = self.font.render('{0} Punkte'.format(self.points), True, (255, 255, 255))
        #hole das Rechteck mit den Daten der Position
        textRect = text.get_rect()
        #Zentriere es auf die werte aus base/settings.py PointsPosition
        textRect.centerx = PointsPosition[0]
        textRect.centery = PointsPosition[1]
        #und Male es
        self.screen.blit(text, textRect)

So die ganzen Klassen sind erstellt und implementiert, nun fehlt nur noch eine Kleinigkeit, nämlich der Code der eine Instanz der Klasse CircleFun erstellt und die mainloop in dieser ausführt:
Code:
#ist dies die ausgefuerte datei?
if __name__ == "__main__":
    #dann initialisiere pygame
    pygame.init()
    #erstelle eine Klasse des Spiels
    circlefun = CircleFun()
    #und durchlaufe die Hauptschleife
    circlefun.mainloop()


Das war es schon; nun haben wir dieses simple Geschicklichkeitsspiel implementiert und das in gerade einmal 151 Quelltextzeilen (+Kommentare & Leerzeilen). Ich hoffe ich konnte euch nahe bringen, wie Mächtig diese kleine Scriptsprache doch ist, und wie schnell man damit etwas implementieren kann.

Anbei gibt es die ganzen Dateien als Zip mit den benötigten Grafiken,…

Viele Grüße,
b2k

PS: Weiterführende Links:
Generell zu Python:
A Byte of Python in Deutsch
Speziell für PyGame
pygame Dokumentation
 

Attachments

  • circlefun.zip
    29.5 KB · Views: 471
Re: Spieleentwicklung mit Python und PyGame

<t>streiche mal das objektorientiert aus dem Thread raus.</t>
 
Re: Objektorientierte Spieleentwicklung mit Python und PyGam

<r>Ist doch nach wie vor objektorientierte und nicht funktionale oder aspektorientierte Programmierung, und somit ist das Objektorientiere im Threadtitel schon ganz ok <E>;)</E></r>
 
Re: Objektorientierte Spieleentwicklung mit Python und PyGam

<t>ist das mit python 2.6.6 oder 3.1 gemacht?<br/>
und wenn ich jetzt dieses byte of python machen möchte, welches sollte ich dann nehmen?</t>
 
Re: Objektorientierte Spieleentwicklung mit Python und PyGam

<t>Das Tutorial ist für Python 2.4 und aufwärts gemacht,<br/>
<br/>
mit Python 3 könnte es funktionieren, aber PyGame ist leider noch nicht 100%ig Python 3 Kompatibel, von daher ist Python 2.6.x vorzuziehen</t>
 
Back
Top