Ork mit Kollisionserkennung
Nachdem ich im letzten Abschnitt gezeigt hatte, wie man einen kleinen Ork mit Hilfe der Pfeiltasten in allen vier Himmelsrichtungen über das Bildschirmfenster jagen kann, bis er am Fensterrand stehenbleibt, möchte ich Euch nun zeigen, wie man eine generelle Kollisionserkennung implementiert. Dafür habe ich erst einmal eine Oberklasse namens Sprite
eingeführt, die das Verhalten, das allen Sprites gemein ist, festlegt und von der alle Sprites erben sollen (zur Bedeutung und Herkunft des Begriffs Sprite informiert die Wikipedia).
Die Klasse Sprite
sieht in Processing.py erst einmal so aus:
class Sprite(object):
def __init__(self, posX, posY):
self.x = posX
self.y = posY
self.dir = 1
self.dx = 0
self.dy = 0
def checkCollision(self, otherSprite):
if (self.x < otherSprite.x + tw and otherSprite.x < self.x + tw
and self.y < otherSprite.y + th and otherSprite.y < self.y + th):
println("Kollision")
return True
else:
return False
Das Objekt wird initialisiert und die Startposition festgelegt. Dann werden noch ein paar Variablen mit Defaultwerten besetzt. Da es durchaus Sprites geben kann, die sich gar nicht bewegen, sind dx
und dy
mit 0
vorbelegt.
Momentan die wichtigste Funktion ist die Funktion checkCollision(self, otherSprite)
. Darin wird geprüft, ob sich die umgebenden Rechtecke der Sprites (in diesem Falle ist das die Bildgröße (tw
und th
sind jeweils 32 Pixel) überlappen, denn dann liegt eine Kollision vor. Dazu ist es für eine einigermaßen »realistische« Darstellung natürlich wichtig, daß die Sprite-Zeichnung das Rechteck möglichst vollständig ausfüllt. In diesem Falle nehme ich das einfach mal an (mehr dazu weiter unten). Die Klasse Orc
erbt nun natürlich von Sprite
:
class Orc(Sprite):
def loadPics(self):
self.orcrt1 = loadImage("orcrt1.gif")
self.orcrt2 = loadImage("orcrt2.gif")
self.orcfr1 = loadImage("orcfr1.gif")
self.orcfr2 = loadImage("orcfr2.gif")
self.orclf1 = loadImage("orclf1.gif")
self.orclf2 = loadImage("orclf2.gif")
self.orcbk1 = loadImage("orcbk1.gif")
self.orcbk2 = loadImage("orcbk2.gif")
def move(self):
if self.dir == 0:
if self.x >= width - tileSize:
self.x = width - tileSize
self.image1 = self.orcrt2
self.image2 = self.orcrt2
else:
self.x += self.dx
self.image1 = self.orcrt1
self.image2 = self.orcrt2
elif self.dir == 1:
if self.y >= height - tileSize:
self.y = height - tileSize
self.image1 = self.orcfr2
self.image2 = self.orcfr2
else:
self.y += self.dy
self.image1 = self.orcfr1
self.image2 = self.orcfr2
elif self.dir == 2:
if self.x <= 0:
self.x = 0
self.image1 = self.orclf2
self.image2 = self.orclf2
else:
self.x -= self.dx
self.image1 = self.orclf1
self.image2 = self.orclf2
elif self.dir == 3:
if self.y <= 0:
self.y = 0
self.image1 = self.orcbk2
self.image2 = self.orcbk2
else:
self.y -= self.dy
self.image1 = self.orcbk1
self.image2 = self.orcbk2
def display(self):
if frameCount % 8 >= 4:
image(self.image1, self.x, self.y)
else:
image(self.image2, self.x, self.y)
hat sich aber ansonsten gegenüber dem letzten Tutorial nicht verändert. Da ja nun die Kollisionsüberprüfung getestet werden muß, habe ich ein weiteres, unbewegliches Sprite konstruiert, das ich aus naheliegenden Gründen Wall
genannt habe. Auch Wall
erbt natürlich von Sprite
:
class Wall(Sprite):
def loadPics(self):
self.pic = loadImage("wall.png")
def display(self):
image(self.pic, self.x, self.y)
Da sich die Mauer nicht bewegt, besitzt Wall
batürlich auch keine move()
-Methode, sondern wird nur angezeigt. Ganz oben in die ersten drei Zeilen des Tabs sprites.py
habe noch ein paar Konstanten initialisiert:
tw = 32
th = 32
tileSize = 32
Das war erst einmal das Modul sprites.py
. Das Hauptprogramm, das ich obstacles
genannt habe, ist immer noch von erfrischender Kürze und dank der Objekte kaum verändert:
tileSize = 32
from sprites import Orc, Wall
def setup():
global bg
bg = loadImage("field.png")
frameRate(30)
size(320, 320)
global orc
orc = Orc(8*tileSize, 0)
orc.loadPics()
orc.dx = 2
orc.dy = 2
global wall1
wall1 = Wall(5*tileSize, 3*tileSize)
wall1.loadPics()
def draw():
global moving
background(bg)
wall1.display()
orc.move()
orc.display()
orc.checkCollision(wall1)
def keyPressed():
if keyPressed and key == CODED:
if keyCode == RIGHT:
orc.dir = 0
elif keyCode == DOWN:
orc.dir = 1
elif keyCode == LEFT:
orc.dir = 2
elif keyCode == UP:
orc.dir = 3
Neu ist lediglich das Mauerfragment wall1
und das nun als letztes in der draw()
-Funktion mit orc.checkCollision(wall1)
überprüft wird, ob unser Ork mit der Mauer kollidiert. Im Falle einer Kollision wird bisher allerdings lediglich »Kollision« in das Terminalfenster geschrieben. Das zeigt, daß der Algorithmus funktioniert, mehr aber noch nicht.
Um dies zu ändern, habe ich erst einmal das println("Kollision")
in der Klasse Sprite
gelöscht und – um auf ein Problem aufmerksam zu machen – die Klasse Tree
als weiteres, unbewegliches Objekt hinzugefügt:
class Tree(Sprite):
def loadPics(self):
self.pic = loadImage("tree.png")
def display(self):
image(self.pic, self.x, self.y)
Bis auf das andere Bildchen unterscheidet sie sich nicht von der Klasse Wall
. Baum und Mauer (sowie die neue Hintergrundkachel) habe ich dem freien (CC BY 3.0) Angband-Tilesets von dieser Site entnommen und mit dem Editor Tiled zurechtgeschnitten. Hier die Bildchen auch für Euch, damit Ihr das Beispiel nachprogrammieren könnt:
Das Hintergrundbild habe ich in Tiled aus der Graskachel erzeugt. Die Bilder des Orks könnt Ihr im letzten Abschnitt finden.
Die Datei im Tab sprites.py
hat sich sonst nicht weiter verändert, aber eine wesentliche Veränderung hat im Hauptprogramm stattgefunden. Hier heißt es nun zwischen orc.move()
und orc.display()
:
if orc.checkCollision(wall1) or orc.checkCollision(tree1):
if orc.dir == 0:
orc.x -= orc.dx
elif orc.dir == 1:
orc.y -= orc.dy
elif orc.dir == 2:
orc.x += orc.dx
elif orc.dir == 3:
orc.y += orc.dy
orc.image1 = orc.image2
Jetzt wird also überprüft, ob eine Kollision mit dem Mauerfragment oder mit dem Baum stattgefunden hat. Hat eine stattgefunden, wird der Orc einfach auf die vorherige Position zurückgesetzt und die beiden Bilder – wie wir es schon mit der Kollision mit den Rändern hatten – auf ein Bild gesetzt, so daß es aussieht, als ob der Ork stehen bleiben würde und auf Eure nächste Eingabe wartet.
Hier nun den kompletten Sketch zum Nachbauen. Erst einmal das Hauptprogramm obstacles02
:
tileSize = 32
from sprites import Orc, Wall, Tree
def setup():
global bg
bg = loadImage("ground0.png")
frameRate(30)
size(320, 320)
global orc
orc = Orc(8*tileSize, 0)
orc.loadPics()
orc.dx = 2
orc.dy = 2
global wall1
wall1 = Wall(5*tileSize, 3*tileSize)
wall1.loadPics()
global tree1
tree1 = Tree(3*tileSize, 7*tileSize)
tree1.loadPics()
def draw():
background(bg)
wall1.display()
tree1.display()
orc.move()
if orc.checkCollision(wall1) or orc.checkCollision(tree1):
if orc.dir == 0:
orc.x -= orc.dx
elif orc.dir == 1:
orc.y -= orc.dy
elif orc.dir == 2:
orc.x += orc.dx
elif orc.dir == 3:
orc.y += orc.dy
orc.image1 = orc.image2
orc.display()
def keyPressed():
if keyPressed and key == CODED:
if keyCode == RIGHT:
orc.dir = 0
elif keyCode == DOWN:
orc.dir = 1
elif keyCode == LEFT:
orc.dir = 2
elif keyCode == UP:
orc.dir = 3
Und dann das Modul sprites.py
, das ich in einem separaten Tab untergebracht habe:
tw = 32
th = 32
tileSize = 32
class Sprite(object):
def __init__(self, posX, posY):
self.x = posX
self.y = posY
self.dir = 1
self.dx = 0
self.dy = 0
def checkCollision(self, otherSprite):
if (self.x < otherSprite.x + tw and otherSprite.x < self.x + tw
and self.y < otherSprite.y + th and otherSprite.y < self.y + th):
return True
else:
return False
class Orc(Sprite):
def loadPics(self):
self.orcrt1 = loadImage("orcrt1.gif")
self.orcrt2 = loadImage("orcrt2.gif")
self.orcfr1 = loadImage("orcfr1.gif")
self.orcfr2 = loadImage("orcfr2.gif")
self.orclf1 = loadImage("orclf1.gif")
self.orclf2 = loadImage("orclf2.gif")
self.orcbk1 = loadImage("orcbk1.gif")
self.orcbk2 = loadImage("orcbk2.gif")
def move(self):
if self.dir == 0:
if self.x >= width - tileSize:
self.x = width - tileSize
self.image1 = self.orcrt2
self.image2 = self.orcrt2
else:
self.x += self.dx
self.image1 = self.orcrt1
self.image2 = self.orcrt2
elif self.dir == 1:
if self.y >= height - tileSize:
self.y = height - tileSize
self.image1 = self.orcfr2
self.image2 = self.orcfr2
else:
self.y += self.dy
self.image1 = self.orcfr1
self.image2 = self.orcfr2
elif self.dir == 2:
if self.x <= 0:
self.x = 0
self.image1 = self.orclf2
self.image2 = self.orclf2
else:
self.x -= self.dx
self.image1 = self.orclf1
self.image2 = self.orclf2
elif self.dir == 3:
if self.y <= 0:
self.y = 0
self.image1 = self.orcbk2
self.image2 = self.orcbk2
else:
self.y -= self.dy
self.image1 = self.orcbk1
self.image2 = self.orcbk2
def display(self):
if frameCount % 8 >= 4:
image(self.image1, self.x, self.y)
else:
image(self.image2, self.x, self.y)
class Wall(Sprite):
def loadPics(self):
self.pic = loadImage("wall.png")
def display(self):
image(self.pic, self.x, self.y)
class Tree(Sprite):
def loadPics(self):
self.pic = loadImage("tree.png")
def display(self):
image(self.pic, self.x, self.y)
Wenn Ihr nun ein wenig damit herumspielt, werdet Ihr eine kleine Ungenauigkeit bemerken. Nähert sich der Ork von rechts oder von links der Tanne, dann sieht es so aus, als ob er ziemlich weit davor stehenbleiben würde. Das liegt daran, daß sowohl die Seitenansichten des Ork wie auch die der Tanne die 32-Pixel Breite nicht besonders gut ausfüllen. Abhilfe könnte man schaffen, indem man die umgebenden Rechtecke schmaler macht. Das ist noch relativ einfach zu implementieren, macht den Quellcode aber dennoch komplizierter und unübersichtlicher. Da ich aber erst einmal nur das Prinzip der Kollisionserkennung mit überlappenden Rechtecken deutlich machen wollte, dachte ich, daß man im Sinne der Klarheit mit diesem kleinen Handicap leben kann.