Einführung
Pygame ist eine Bibliothek in Python, die es erlaubt grafisch anspruchsvolle Spiele zu erstellen. Damit pygame effektiv eingesetzt werden kann, braucht es doch einiges an Basiswissen, das in diesem Blog vermittelt wird. Gerade für Spiele wird vielmals von Elementen der Künstlichen Intelligenz Gebrauch gemacht. Teilweise handelt es sich nur um offensichtliche KI-Routinen aber andererseits auch um hochkomplexe Algorithmen, die nicht so trivial zu verstehen sind. Vor etlichen Jahren habe ich ein Buch gekauft, mit dem ansprechenden Namen "AI for Game developers" von David M. Bourg und Glenn Seemann. Das Ziel war es, herauszufinden, wie einfach ein Spiel unter Java 5 realisiert werden könnte. Das ganze sollte ein Kundenprojekt werden, ist aber dann nie zustande gekommen, weil der Aufwand zu gross war, das Projekt mit den bestehenden Java Bibliotheken umzusetzen. Für meine weitere "Forschung" hinsichtlich KI habe ich dieses Buch wieder zur Hand genommen und es zeigt sich, dass mit pygame die dort vorgestellten Algorithmen erheblich leichter implementiert werden können, weil der Programmierer sich nicht mit komplexen Grafikroutinen, Gameloops, etc. herumschlagen muss. Ich hoffe, noch im Verlauf dieses Jahres einige in diesem Buch vorhandene Elemente auf meinem Blog zu veröffentlichen.
Übersicht pygame
Das Modul pygame hilft Entwicklern bei der Spieleentwicklung, indem es die Ausgabe von Grafiken und Musik erleichtert. Das Modul gehört nicht direkt zu Python, kann aber problemlos mit pip installiert werden.
Hello World
Unten folgt das obligatorische "Hello World" Programm:
import pygame, sys
from pygame.locals import *
class HelloWorld:
__windowsSurface = 0
# color constants
__BLACK = (0, 0, 0)
__WHITE = (255, 255, 255)
__RED = (255, 0, 0)
__GREEN = (0, 255, 0)
__BLUE = (0, 0, 255)
# font
__basicFont = 0
# text to display
__text = 'Hello world'
# the rectangular text area
__textRect = 0
def __init__(self):
pygame.init()
# Einrichten der Schriftart
self.__basicFont = pygame.font.SysFont(None, 48)
self.__initWindow()
self.__displayCaptions()
self.__initText()
def __initWindow(self):
# Richtet das Fenster ein
self.__windowsSurface = pygame.display.set_mode((500, 400), 0, 32)
def __displayCaptions(self):
# Setzt den Titel im Fenster
pygame.display.set_caption(self.__text)
def __initText(self):
# Gibt ein gerendertes aber nicht visuell sichtbares textfeld zurueck mit
# weisser Schriftfarbe und blauem Hintergrund
t = self.__basicFont.render(self.__text, True, self.__WHITE, self.__BLUE)
# Abfragen der Groesse des gerenderten Objekts
self.__textRect = t.get_rect()
# Umplatzieren in Mitte des Fensters
self.__textRect.centerx = self.__windowsSurface.get_rect().centerx
self.__textRect.centery = self.__windowsSurface.get_rect().centery
# Ein zusätzliches Rechteck um den Text malen
pygame.draw.rect(self.__windowsSurface,
self.__RED, (self.__textRect.left - 20,
self.__textRect.top - 20,
self.__textRect.width + 40,
self.__textRect.height + 40))
# den Text auf die WindowsSurface malen
self.__windowsSurface.blit(t, self.__textRect)
def play(self):
pygame.display.update()
while True:
for event in pygame.event.get():
# bei Schliessen des Fensters Anwendung beenden
if event.type == QUIT:
pygame.quit()
sys.exit()
helloWorld = HelloWorld()
helloWorld.play()
Im oberen Teil des Programms wird im Konstruktor die Game-Engine von pygame initialisiert. Dann werden einige Initialisierungsaufgaben durch den Konstruktor auf andere in der Klasse vorhandene Methoden delegiert. Das wichtigste Objekt ist das Surface Objekt, das die eigentliche Zeichenfläche darstellt auf das dann andere Objekte gezeichnet werden. Das Rendering des Textes findet Off-Screen statt, das heisst das Objekt wird vor dem Zeichnen schon mal gerendert. Mit der Methode blit der Surface wird dann das Textobjekt gezeichnet. Das Resultat sieht wie folgt aus:
Eine erste Animation
In einem nächsten Schritt definieren wir ein weiteres Objekt - einen grünen Kreis - und lassen diesen um den Schriftzug "Hello world" rotieren. Dieses Beispiel dient nur dazu zu demonstrieren, das mit pygame sehr einfach elementare Formen gezeichnet werden können. Der wichtige Teil ist aber sicherlich die Animation, das heisst das Drehen des Objekts um den Schriftzug.Diese Animation kann mittels elementarer Grafik oder aber mit Sprites erreicht werden. Da Sprites eine Abstrahierung bewegter Objekte sind, sind diese sicher zu bevorzugen. Im Codebeispiel unten wird der eine Kreis elementar gezeichnet und der andere als Sprite. Insgesamt sind drei Python Dateien vonnöten, um den Code zu implementieren:
Datei: game1.py
import pygame, sys
from pygame.locals import *
import math
import Circle
import time
class HelloWorld:
__windowsSurface = 0
# color constants
__BLACK = (0, 0, 0)
__WHITE = (255, 255, 255)
__RED = (255, 0, 0)
__GREEN = (0, 255, 0)
__BLUE = (0, 0, 255)
# font
__basicFont = 0
# text to display
__TEXT = 'Hello world'
# the rectangular text area
__textRect = 0
# the radius of the circle
__radius = 0
# the off screen images
__image1 = 0
__image2 = 0
def __init__(self):
pygame.init()
# Einrichten der Schriftart
self.__basicFont = pygame.font.SysFont(None, 48)
self.__initWindow()
self.__displayCaptions()
self.__initText()
def __initWindow(self):
# Richtet das Fenster ein
self.__windowsSurface = pygame.display.set_mode((500, 400), 0, 32)
def __displayCaptions(self):
# Setzt den Titel im Fenster
pygame.display.set_caption(HelloWorld.__TEXT)
def __initText(self):
# Gibt ein gerendertes aber nicht visuell sichtbares textfeld zurueck mit
# weisser Schriftfarbe und blauem Hintergrund
t = self.__basicFont.render(HelloWorld.__TEXT, True, HelloWorld.__WHITE, HelloWorld.__BLUE)
# Abfragen der Groesse des gerenderten Objekts
self.__textRect = t.get_rect()
# Umplatzieren in Mitte des Fensters
self.__textRect.centerx = self.__windowsSurface.get_rect().centerx
self.__textRect.centery = self.__windowsSurface.get_rect().centery
# Ein zusätzliches Rechteck um den Text malen
pygame.draw.rect(self.__windowsSurface,
HelloWorld.__RED, (self.__textRect.left - 20,
self.__textRect.top - 20,
self.__textRect.width + 40,
self.__textRect.height + 40))
# den Text auf die WindowsSurface malen
self.__windowsSurface.blit(t, self.__textRect)
def __assignImage(self, coluor):
image = pygame.Surface((50, 50))
image.fill(HelloWorld.__BLACK)
image.set_colorkey(HelloWorld.__BLACK)
pygame.draw.circle(image, coluor, (25, 25), 20, 0)
image.convert()
return image
def __drawCircle(self, x, y, coluor, imageindex):
image = 0
if imageindex == 1:
if self.__image1 == 0:
image = self.__image1 = self.__assignImage(coluor)
else:
image = self.__image1
if imageindex == 2:
if self.__image2 == 0:
image = self.__image2 = self.__assignImage(coluor)
else:
image = self.__image2
draw_area = image.get_rect().move(x - 25, y - 25)
return image, draw_area
def play(self):
# mal alles Zeichnen, was schon vorhanden ist
pygame.display.update()
# Variablen fuer die Kreisbewegung definieren
theta = 0
radius = 150
steps = 500
# Definition des Kreises als Sprite
sprite = Circle.Circle(HelloWorld.__GREEN)
oldx = oldy = -1
while True:
for event in pygame.event.get():
# bei Schliessen des Fensters Anwendung beenden
if event.type == QUIT:
pygame.quit()
sys.exit()
# neue Position des Sprites berechnen
sprite.update(radius, theta, (self.__textRect.centerx, self.__textRect.centery))
# fuer den Kreis ohne Sprite die neue Position berechnen
y = radius * math.cos(theta + math.pi) + self.__textRect.centery
x = radius * math.sin(theta + math.pi) + self.__textRect.centerx
# Bild des Kreises Offscreen zeichnen
surface, area = self.__drawCircle(x, y, HelloWorld.__BLUE, 1)
self.__windowsSurface.blit(surface, area)
# Bild des Sprites Offscreen zeichnen
self.__windowsSurface.blit(sprite.image, sprite.get_rect())
# Offscreen Images darstellen
pygame.display.update([area, sprite.get_rect()])
pygame.display.flip()
time.sleep(0.008)
# fuer das Loeschen des gruenen Kreises kann ein neuer schwarzer Kreis gezeichnet werden
# surface, area = self.__drawCircle(x, y, HelloWorld.__BLACK, 2)
# self.__windowsSurface.blit(surface, area)
# effizienter ist aber das Quadrat in dem der Kreis abgebildet ist zu loeschen
self.__windowsSurface.fill([0, 0, 0], sprite.get_rect())
self.__windowsSurface.fill([0, 0, 0], area)
theta = (2 * math.pi/steps + theta) % (2*math.pi)
helloWorld = HelloWorld()
helloWorld.play()
Datei Circle.py
import pygame
import math
class Circle(pygame.sprite.Sprite):
def __init__(self, color):
super().__init__()
self.image = pygame.Surface([50, 50])
self.image.fill((0, 0, 9))
self.image.set_colorkey((0, 0, 0))
pygame.draw.circle(self.image, color, (25, 25), 20, 0)
self.image.convert()
self.rect = self.image.get_rect()
def get_rect(self):
return self.rect
def update(self, *args):
radius = args[0]
theta = args[1]
centerx, centery = args[2]
y = radius * math.cos(theta) + centery
x = radius * math.sin(theta) + centerx
self.rect = self.image.get_rect().move(x-25, y-25)
Datei __init__.py
from .Circle import Circle
Damit das ganze Beispiel lauffähig ist sollten sie für __init__.py und Circle.py in PyCharm ein eigenes Package mit dem Namen Circle definieren.
Das Resultat sieht dann wie folgt aus. Leider sieht man unschön, dass die Bewegungen nicht ganz optimal ablaufen. Das kann aber darauf zurückgeführt werden, dass ich die Aufnahme in einer Windows XSession gemacht habe. Die Übertragung der Bilddaten auf den Arbeitscomputer sind in einer XSession nie ideal.
Noch beinhalteten unsere Beispiele überhaupt keine Künstliche Intelligenz. Ziel ist es hier auch nicht ein vollständiges Spiel zu entwickeln, sondern in den weiteren Betrachtungen, nach und nach Elemente der KI in die Programmierung einfliessen zu lassen.
Manche Spielprogrammierer reden schon von KI, wenn es um die Detektion von Kollisionen zwischen Objekte geht. Nachfolgender Abschnitt befasst sich genau mit diesem Thema. Es wird eine eigene Methode - nicht allgemein - und eine Methode, die pygame zur Verfügung stellt, vorgestellt. In einem einfachen Spiel kann eine Kollision als der Punkt im Spiel bezeichnet werden, wo das Spiel endet, indem zum Beispiel der Gnome den Ritter gefressen hat. Kollisionserkennung ist sicher wichtig in einem Spiel, hat aber noch nichts mit intelligentem Verhalten zu tun. Erst beim sogenannten Chasing and Evading, wo der Gegner aktiv auf die eigene Figur zusteuert, deutet der Computer so etwas wie intelligentes Verhalten an. Der Gegner benutzt nämlich ein mehr oder weniger komplexes Suchmuster, um das Spiel zu seiner eigener Gunst zu entscheiden. Daher setzen wir uns nach der Kollisionserkennung intensiv mit dem Thema Chasing and Evading auseinander.
Kollisionserkennung
In diesem Beispiel kehren wir dem vorherigen Beispiel den Rücken und lassen uns vorerst durch folgende Animation begeistern.
Damit die Kugeln reibungslos voneinander abprallen, überprüfen wir bei jeder Neudarstellung des Bildschirminhalts, ob die Kugeln kollidieren. Wenn dies der Fall ist, drehen wir die Richtung um 180 Grad um. Pygame hat vom Umfang her, eine eigene Kollisionserkennung integriert und von dieser kann dann Gebrauch gemacht werden. Rein didaktisch ist es aber trotzdem illustrativ, eine eigene Kollisionserkennung zu implementieren. Vom Prinzip her, wählt man ein Objekt (Target) aus, welches sich im Raum bewegt. Dann wird überprüft, ob sich die anderen Objekte bei einem nächsten Bewegungsschritt in das Objekt hineinbewegen. Unabhängig davon, ob es sich um Kreise, Quadrate oder Rechtecke handelt, werden in Pygame die Objekte als Rechtecke dargestellt. Daher genügt es, die Kollisionserkennung für Rechtecke zu schreiben. In folgender Grafik bewegen sich vier Objekte auf das Target zu. Es muss dann für jedes Objekt überprüft werden, ob das Target gemeinsame x- oder y- Koordinaten mit dem anderen Objekt hat. Normalerweise genügt das für Objekte gleicher Grösse. Haben aber die Objekte alle eine unterschiedliche Grösse, so ist es manchmal notwendig, dass Target mit dem Objekt zu wechseln, um tatsächlich eine Kollision festzustellen.
Objekte bewegen sich auf das Target zu |
Kehren der Rolle von Target und Objekt |
Eine eigene Kollisionserkennung implementieren
Zuerst generieren wir alle Objekte und platzieren sie links oben in der Windows-Oberfläche (Surface):
def __createImages(self):
i = 0
counter = random.randint(5, 15)
while i < counter:
size = random.randint(18, 60)
color = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))
image = self.__drawCircle(color, (size, size))
self.__images.append(image)
self.__areas.append(image.get_rect())
i += 1
In einem nächsten Schritt verteilen wir alle Objekte auf der Surface und zwar so, dass sie sich nicht schneiden.
def __randomPaintImages(self):
i = 0
while i < len(self.__images):
surface = self.__images[i]
area = self.__areas[i]
pos = (random.randint(area.width, 500 - area.width), random.randint(area.height, 400 - area.height))
area.centerx = pos[0]
area.centery = pos[1]
self.__areas[i] = area
indexList = self.__detectCollision(surface, area)
while len(indexList) > 0:
pos = (random.randint(area.width, 500 - area.width), random.randint(area.height, 400 - area.height))
area.centerx = pos[0]
area.centery = pos[1]
self.__areas[i] = area
indexList = self.__detectCollision(surface, area)
i += 1
# area = surface.get_rect().move(pos)
self.__windowsSurface.blit(surface, area)
Der Algorithmus dies zu tun - wie oben angegeben - ist sicherlich nicht optimal. Es wird ein Objekt genommen (aus der Objektliste) und dann ein Zufallsposition innerhalb des sichtbaren Windows-Bereichs zugeordnet. Mit der Methode detectCollision wird überprüft, ob ein Objekt mit einem anderen Objekt "zusammenstösst". Ist dies der Fall, so wird dem Objekt eine neue Zufallsposition zugeordnet und zwar so lange, bis es nicht mehr mit anderen Objekten überlappt.
Unten der Algorithmus, um Kollisionen zu erkennen. Der Methode detectCollision wird das Target und dessen aktuelle Position übergeben. Um auch Sonderfälle abzudecken, wird die eigentliche Kollisionserkennung zweimal ausgeführt. Einmal in der Rolle Target - Objekt und das andere Mal in vertauschter Rolle.
Die Methode gibt eine Liste von Indices zurück, mit Angabe der Objektpositionen innerhalb der Objektliste. Somit wissen wir, welches Objekt mit welchem Objekt kollidiert.
def __detectCollision(self, image, rect):
area = rect.copy()
result = []
index1 = self.__images.index(image)
j = 0
i = 0
while i < len(self.__areas):
drawarea = self.__areas[i]
index2 = j
t = 0
while t < 2:
if area.right in range(drawarea.left, drawarea.right + 1):
if area.top in range(drawarea.top, drawarea.bottom + 1):
if index1 != index2:
result.append((index1, index2))
if area.bottom in range(drawarea.top, drawarea.bottom + 1):
if index1 != index2:
result.append((index1, index2))
if area.left in range(drawarea.left, drawarea.right + 1):
if area.top in range(drawarea.top, drawarea.bottom + 1):
if index1 != index2:
result.append((index1, index2))
if area.bottom in range(drawarea.top, drawarea.bottom + 1):
if index1 != index2:
result.append((index1, index2))
temparea = area.copy()
area = drawarea.copy()
drawarea = temparea
tempIndex = index2
index2 = index1
index1 = tempIndex
t += 1
i += 1
j += 1
return result
Unten der Algorithmus, um die Bilder zu bewegen. Im Prinzip wird ein Richtungsvektor innerhalb des Einheitskreises festgelegt. Danach wird der Vektor noch skaliert. Mit der Methode move des Rect Objekts kann dann eine hochaufgelöste Pixelbewegung erzielt werden.
def __moveImages(self):
if len(self.__dvectors) == 0:
count = len(self.__images)
i = 0
while i < count:
x = y = 0
theta = random.random() * math.pi * 2
if math.cos(theta) > 0:
x = round(math.cos(theta) + 1)
else:
x = round(math.cos(theta) - 1)
if math.sin(theta) > 0:
y = round(math.sin(theta) + 1)
else:
y = round(math.sin(theta) - 1)
self.__dvectors.append((x, y))
i += 1
i = 0
while i < len(self.__areas):
area = self.__areas[i]
vector = self.__dvectors[i]
done = False
while area.top <= 0:
if not done:
self.__dvectors[i] = (vector[0], -vector[1])
vector = self.__dvectors[i]
done = True
area = area.move(vector[0], 2)
while area.bottom >= 400:
if not done:
self.__dvectors[i] = (vector[0], -vector[1])
vector = self.__dvectors[i]
done = True
area = area.move(vector[0], -2)
while area.left <= 0:
if not done:
self.__dvectors[i] = (-vector[0], vector[1])
vector = self.__dvectors[i]
done = True
area = area.move(2, vector[1])
while area.right >= 500:
if not done:
self.__dvectors[i] = (-vector[0], vector[1])
vector = self.__dvectors[i]
done = True
area = area.move(-2, vector[1])
if done:
self.__windowsSurface.blit(self.__images[i], area)
self.__areas[i] = area
continue
vector = self.__dvectors[i]
area = area.move(vector[0], vector[1])
indexes = self.__detectCollision(self.__images[i], area)
if len(indexes) > 0:
vector = self.__dvectors[i]
self.__dvectors[i] = (-vector[0], -vector[1])
area = area.move(-vector[0], -vector[1])
self.__areas[i] = area
self.__windowsSurface.blit(self.__images[i], area)
self.__areas[i] = area
i += 1
Unten noch der Play-Alogorithmus:
def play(self):
# mal alles Zeichnen, was schon vorhanden ist
self.__randomPaintImages()
pygame.display.update()
while True:
for event in pygame.event.get():
# bei Schliessen des Fensters Anwendung beenden
if event.type == QUIT:
pygame.quit()
sys.exit()
self.__moveImages()
pygame.display.flip()
self.__windowsSurface.fill([0, 0, 0])
time.sleep(0.008)
game = CollisionDetection()
game.play()
Verwenden der Kollisionserkennung von Pygame
Die Animation oben basierte auf der eigenen Kollisionserkennung. Pygame stellt aber eine eigene Kollisionserkennung zur Verfügung. Diese diskutieren wir rasch:<<to be continued>>
Keine Kommentare:
Kommentar veröffentlichen