- 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
(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:
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.
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.
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:
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.
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:
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
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)
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