Nach den ersten beiden einführenden Tutorials in Cocos2d (Python) möchte ich nun in medias res gehen und ein kleines »Spiel« mit Cocos2d entwickeln, das in die Möglichkeiten dieses Frameworks einführt. Es ist noch kein echtes Spiel (dafür ist es zu wenig herausfordernd), aber ein paar wichtige Grundlagen wie die Erkennung von Tastatureingaben und von Kollisionen sollen anhand dieses Beispiels erläutert werden.
Die schon aus einigen meiner Tutorials bekannte Spielfigur »Hörnchen« soll auf einem Spielfeld vier Herzen einsammeln. Die Startposition des Spiels ist aus dem obigen Screenshot ersichtlich. Zu Beginn verwende ich das Programmgerüst aus dem letzten Tutorial, nur daß ich das Fenster auf 640x480 Pixel vergrößert und zwei Funktionen, die den Tastatur-Input verarbeiten, eingefügt habe:
import cocos
from cocos.director import director
from cocos.scene import Scene
from cocos.layer import ColorLayer
from cocos.sprite import Sprite
from pyglet.window import key
class Game(ColorLayer):
is_event_handler = True
def __init__(self):
super(Game, self).__init__(0, 80, 125, 255)
self.player = Sprite("assets/horngirl.png")
self.player.position = 320, 240
self.add(self.player)
def on_key_press(self, k, m):
print("Pressed", key.symbol_string(k))
def on_key_release(self, k, m):
print("Released", key.symbol_string(k))
def main():
director.init(caption = "Moving Kitty", width = 640, height = 480)
scene = Scene(Game())
director.run(scene)
if __name__ == "__main__":
main()
Die Angabe der Fenstergröße wäre nicht nötig gewesen, den 640x480 Pixel ist die Default-Größe, aber der Ordnung halber und falls ich doch irgendwann die Größe des Fensters verändern will, habe ich sie angegeben.
Wenn Ihr mit den Koordinaten des Players (also »Hörnchen«) in der Zeile
self.player.position = 320, 240
ein wenig herumspielt (setzt sie zum Beispiel mal auf 50, 50
oder 590, 430
), dann erkennt Ihr, daß das Cocos2d-Koordinatensysten seinen Urpsrung in der linken, unteren Ecke des Fensters hat und nicht – wie allgemein üblich – in der linken, oberen Ecke. Das ist erst einmal ungewohnt, aber für Spiele in der Regel doch sinnvoll. Man erspart sich einige Umrechnungen und Transformationen.
Außerdem sind die Sprite-Koordinaten per Default im Mittelpunkt des Sprites und nicht, wie in vielen anderen Bibliotheken, in der linken oberen Ecke.
Jeder Layer von Cocos2d kennt fünf verschiedene Event Handler, die auf die Eingabe des Nutzers reagieren. Diese sind jedoch per default auf False
gesetzt und müssen daher mit der Zeile
is_event_handler = True
scharf geschaltet werden. Diese fünf Event Handler sind:
on_mouse_motion(x, y, dx, dy)
: Hier sind x, y
die aktuellen Mauskoordinaten und dx, dy
die Distanz seit dem letzten Funktionsaufruf.
on_mouse_press(x, y, buttons, modifiers)
: Wie oben sind x, y
die aktuellen Mauskoordinaten, jedoch werden diese nur übergeben, wenn eine Maustaste gedrückt wird.
on_mouse_drag(x, y, dx, dy, button, modifiers)
: Diese Funktion ist eine Kombination der beiden obigen Funktionen, die aufgerufen wird, wenn die Maus mit einer oder mehrerer gedrückter Tasten bewegt wird.
on_key_press(key, modifiers)
: Diese Funktion wird aufgerufen, wenn eine Taste gedrückt wird.
on_key_release(key, modifiers)
: Diese Funktion wird aufgerufen, wenn eine Taste losgelassen wird.
Die Parameter button
und modifiers
kommen aus pyglet.window.mouse
und pyglet.window.key
und betreffen die Maustasten und die Sondertasten, wie zum Beispiel Shift, Alt oder die Pfeiltasten. Da ich diese für das Beispielprogramm benötige, habe ich die entsprechende PyGlet-Konstanten mit der Zeile
from pyglet.window import key
importiert. Überhaupt ist PyGlet so eng verzahnt mit Cocos2d, daß immer wieder PyGlet-Konstanten oder -Funktionen in einem Cocos2d-Programm aufgerufen werden müssen.
Wie man in obigem Screenshot sehen kann, funktioniert so die Keyboard-Abfrage auch mit den Sondertasten wie den Pfeiltasten oder der Leertaste. Für Spiele ein nicht unwichtiger Faktor.
Nun soll unsere Spielfigur auf die Tastatursteuerung reagieren und sich in Richtung der Pfeiltasten bewegen. Dazu habe ich das Programm noch einmal erweitert:
import cocos
from cocos.director import director
from cocos.scene import Scene
from cocos.layer import ColorLayer
from cocos.sprite import Sprite
from collections import defaultdict
from pyglet.window import key
class Game(ColorLayer):
is_event_handler = True
def __init__(self):
super(Game, self).__init__(0, 80, 125, 255)
self.player = Sprite("assets/horngirl.png")
self.player.position = 320, 240
self.add(self.player)
self.speed = 100.0
self.pressed = defaultdict(int)
self.schedule(self.update)
def on_key_press(self, k, m):
self.pressed[k] = 1
def on_key_release(self, k, m):
self.pressed[k] = 0
def update(self, dt):
x = self.pressed[key.RIGHT] - self.pressed[key.LEFT]
y = self.pressed[key.UP] - self.pressed[key.DOWN]
if (x != 0) or (y != 0):
pos = self.player.position
new_x = pos[0] + self.speed * x * dt
new_y = pos[1] + self.speed * y * dt
self.player.position = (new_x, new_y)
def main():
director.init(caption = "Moving Kitty", width = 640, height = 480)
scene = Scene(Game())
director.run(scene)
if __name__ == "__main__":
main()
Zuerst habe ich aus collections
zusätzlich das defaultdict
importiert, eine Datenstruktur, genauer ein Dictionary, in das die Werte den entsprechenden Tastaturnamen zugeordnet werden können. Das geschieht in den Funktionen on_key_press()
und on_key_release()
.
Die update()
-Funktion macht es sich durch einen Trick besonders einfach: Da eine Taste, die nicht gedrückt ist, immer den Wert 0
besitzt, sind die Werte von x
und y
immer 1
, 0
oder -1
. Das wird durch die Zeilen
x = self.pressed[key.RIGHT] - self.pressed[key.LEFT]
y = self.pressed[key.UP] - self.pressed[key.DOWN]
erreicht. Und nur wenn ener der Werte ungleich 0
ist, wird eine neue Position berechnet und das Hörnchen durch das Fenster bewegt.
Caveat: Es gibt keine Abfrage der Fenstergrenzen, Ihr könnt also die Spielfigur auch außerhalb des Fensters schieben.
Die Kollisions-Erkennung von Cocos2d wird im Paket cocos.collision_model
durch die Klassen des CollsionManager
erledigt. Also gilt es zuerst, das Paket zu importieren:
import cocos.collision_model as cm
Der CollisionManager
ist in zwei Klassen aufgeteilt:
Der CollisionManagerBruteForce
beachtet alle Entities und berechnet eine mögliche Kollision. Die Klasse ist nur für Debugging-Zwecke gedacht, da sie schlecht skaliert, wenn die Zahl der möglichen Kollisionen wächst.
Das CollisionManagerGrid
teilt das Fenster in rechteckige Zellen auf und berücksichtigt nur die Entities für eine mögliche Kollision, die mit dem Spieler die gleiche Zelle teilen. Das skaliert natürlich viel besser. Die empfohlene Zellgröße ist die Breite und Höhe des größten Actors, multipliziert mit 1,25.
Jedes Objekt wird für eine mögliche Kollision in Betracht gezogen, wenn es eine Instanz von Cshape
besitzt, die ebenfalls aus dem Paket cocos.collsion_model
importiert wird. Es gibt zwei Klassen in Cocos2d, die von Cshape erben:
CircleShape
: Hier wird ein Kreis um die Spielfigur gezogen und wenn sich zwei Kreise überlappen, das heißt, wenn der Abstand beider Kreise kleiner ist als die Summe beider Radii, wird eine Kollision festgestellt. Abstandsfunktion ist der einfache euklidische Abstand.
AARectShape
: Hier wird ein Rechteck um die Spielfigur gezogen und wenn sich beide Rechtecke überlappen, wird eine Kollision festgestellt. Diese Kollisionserkennung funktionert sehr gut, wenn die Spielfiguren eng ein Quadrat ausfüllen oder wenn sie nicht rotieren. Als Abstandsfunktion wird hierbei die Manhattan-Distanz genutzt.
Für das Beispielprogramm habe ich den CircleShape
gewählt und da dieser cocos.euclid.Vector2
nutzt, habe ich diesen in der Zeile
import cocos.euclid as eu
ebenfalls importiert. Mit den beiden Zeilen
self.position = pos = eu.Vector2(x, y)
self.cshape = cm.CircleShape(pos, self.width/2 - 10)
habe ich den CircleShape
implementiert. Damit es keine Fernkollisionen gibt, habe ich den Radius der Spielfigur um 10 Pixel verkleinert um sicherzugehen, daß Hörnchen auch tatsächlich die Herzen berührt, die sie aufsammelt. In der __init__()
-Methode der Klasse Game
habe ich mit den Zeilen
cell = self.player.width * 1.25
self.collman = cm.CollisionManagerGrid(0, 640, 0, 480, cell, cell)
die Zellengröße festgelegt und den Kollisions-Manager initialisiert. Und die update()
-Methode erhielt die folgenden Zeilen,
self.collman.clear()
for _, node in self.children:
self.collman.add(node)
for other in self.collman.iter_colliding(self.player):
self.remove(other)
mit der die möglichen Kollisionen überprüft werden und die nach einer Kollision die entsprechenden Herzchen verschwinden.
Hier nun der vollständige Quellcode des fertigen Programms:
import cocos
from cocos.director import director
from cocos.scene import Scene
from cocos.layer import ColorLayer
from cocos.sprite import Sprite
import cocos.collision_model as cm
import cocos.euclid as eu
from collections import defaultdict
from pyglet.window import key
class Actor(Sprite):
def __init__(self, x, y, pic):
super(Actor, self).__init__(pic)
self.position = pos = eu.Vector2(x, y)
self.cshape = cm.CircleShape(pos, self.width/2 - 10)
class Game(ColorLayer):
is_event_handler = True
def __init__(self):
super(Game, self).__init__(0, 80, 125, 255)
self.heart_pic = "assets/heart.png"
for pos in [(100, 100), (540, 380), (540, 100), (100, 380)]:
self.add(Actor(pos[0], pos[1], self.heart_pic))
self.player = Actor(320, 240, "assets/horngirl.png")
self.add(self.player)
cell = self.player.width * 1.25
self.collman = cm.CollisionManagerGrid(0, 640, 0, 480, cell, cell)
self.speed = 100.0
self.pressed = defaultdict(int)
self.schedule(self.update)
def on_key_press(self, k, m):
self.pressed[k] = 1
def on_key_release(self, k, m):
self.pressed[k] = 0
def update(self, dt):
# print(dt)
self.collman.clear()
for _, node in self.children:
self.collman.add(node)
for other in self.collman.iter_colliding(self.player):
self.remove(other)
x = self.pressed[key.RIGHT] - self.pressed[key.LEFT]
y = self.pressed[key.UP] - self.pressed[key.DOWN]
if (x != 0) or (y != 0):
pos = self.player.position
new_x = pos[0] + self.speed * x * dt
new_y = pos[1] + self.speed * y * dt
self.player.position = (new_x, new_y)
self.player.cshape.center = self.player.position
def main():
director.init(caption = "Kitty sammelt Herzen", width = 640, height = 480)
scene = Scene(Game())
director.run(scene)
if __name__ == "__main__":
main()
Cocos2d zeichnet die einzelnen Sprites in der Reihenfolge ihres Auftretens. Daher habe ich zuerst die Herzchen und dann das Hörnchen zeichnen lassen. Damit ihr das »Spiel« nachprogrammieren könnt, hier auch noch die dafür benötigten Bilder:
Hörnchen alias »Horn Girl« und das Herz stammt wieder aus dem von Daniel Cook (Danc) in seinem Blog Lost Garden unter einer freien Lizenz (CC BY 3.0 US) zu Verfügung gestellten Tileset Planet Cute. Und das Beispielprogramm ist eine aufgebohrte Fassung eines Programms aus dem wunderbaren Buch Python Game Programming by Example von Alejandro Rodas de Paz und Joseph Howse, das in drei ausführlichen Kapiteln sie Spieleprogrammierung mit Cocos2d (Python) behandelt.
Wie schon angekündigt, habe ich eine Site »Learning Cocos2d« hochgezogen und auch dieses Tutorial dort hochgeladen. Korrekturen, Änderungen und Ergänzungen wird es daher nur dort geben.
Ü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