Das Regentropfenspiel wurde bereits in Make Code mit Sprite-Objekten realisiert. Leider
sind die Sprite-Objekte recht unflexibel und lassen sich nicht an einen größeren Bildschirm anpassen.
Deshalb wird in Python ein eigenes Sprite-Objekt entwickelt, das entsprechend den Wünschen des Programmierers angepasst werden kann. Die folgenden Anleitungen realisieren nur einige Grundfunktionen, die aber für ein einfaches Spiel ausreichen.
In ersten Teil wird das Spiel auf dem 5x5-Display des Calliope mini gespielt. Im zweiten Teil wird als Spielfläche eine 8x8-Neopixel-Matix verwendet. Das ist ein wenig aufwendiger, da die 64 LED nicht direkt über x- und y-Koordinaten angesprochen werden können und für die Farbe eine HSL-zu RGB-Konvertierung verwendet wird.
In der Datei sprite.py wird eine Klasse sprite definiert. Wird dann im Hauptrogramm eine Instanz dieser Klasse erzeugt, wird automatisch die Methode __init__ aufgerufen. Diese Methode bekommt für den leuchtenden Punkt die x- und y- Koordinate übergeben. Die Richtung wird auf 1 festgesetzt.
Auf das Attribut richtung greift die Methode bewege() zu. Diese Methode wird als nächstes der Klasse zugefügt.
Festlegung für die Richtung:
Im Hauptprogramm wird die Datei sprite.py importiert und ein Objekt wasser erzeugt. Das erste sprite ist die Datei, in der die Klasse steckt und das zweite sprite der Name der Klasse, von dem das Objekt erzeugt wird.
Da die Methode __init__ die Koordinaten des leuchtenden Punktes erwartet, wird x=2 und y=0 übergeben. Das Programm lässt dann wirklich oben in der Mitte die LED aufleuchten.
Im nächsten Schritt wird der Klasse sprite die Methode bewege() hinzugefügt. Sie bewegt den leuchtenden Punkt entsprechend der Richtung um eine Stelle weiter. Dabei achtet sie auch ganz rührend darauf, das der Kobold nicht über den Spielfeldrand fällt und verschwindet.
Als erstes wird der Punkt an der aktuellen Stelle gelöscht. Dann werden die neuen Koordinaten berechnet und der Punkt leuchtet an dieser Stelle wieder auf.
Hinweis: Falls man später die Klasse ausbauen möchte, kann man auch diagonale Richtungen ermöglichen.
Lässt man das Programm nun eine halbe Sekunde ruhen und bewegt dann den Punkt, wandert er erwartungsgemäß eine Stelle nach rechts.
Damit der Tropfen nach unten fällt, muss die Richtung geändert werden. Das Attribut __richtung ist von außen über eine weitere Methode erreichbar.
Im nächsten Schritt soll es nun endlich von der Decke tropfen. Dazu braucht man die volle Kontrolle über die x- und y-Koordinaten des Objektes. Es werden dazu in der Klassendefinition 4 weitere Methoden geschrieben. Die ersten beiden liefern einfach nur die x- und y-Werte zurück.
Die setze-Methoden prüfen zuerst, ob die übergebenen Werte in das Raster der LED-Matrix passen. Wenn das der Fall ist, wird der Punkt an der aktuellen Position gelöscht, die Werte geändert und der Punkt an der neuen Position zum Leuchten gebracht.
Es reichen dann wenige Programmzeilen aus, um es pausenlos von der Decke tropfen zu lassen.
Unschön ist jetzt nur noch der sleep(500)-Befehl am Ende des Programms. Es soll ja ein Eimer in der unteren Zeile mit den Tasten A und B so verschoben werden, dass die Tropfen gefangen werden. Leider reagiert der Calliope mini in der Schlafzeit nicht auf die Tasten, er schläft ja tief und fest.
Das Problem kann mit dem Befehl running_time() gelöst werden. Dieser Befehl liefert die Laufzeit des Calliope mini in Millisekunden seit dem Start des Programms. Die Variable zeit enthält in ms den Zeitpunkt der nächsten Bewegung. Ist er erreicht, wird die Variable zeit um den Wert aus wartezeit erhöht.
Wenn alles richtig läuft, tropft das Wasser wieder im Halbsekundentakt. Der Calliope mini arbeitet aber durch und hat keine Schlafenszeit.
Nun praucht man noch einen Eimer, der die Tropfen in der unteren Zeile fängt. Der Eimer ist eine weitere Instanz der sprite-Klasse und taucht sofort am unteren Rand auf.
Der Eimer soll bei jedem Druck auf die Taste A oder B um genau eine Stelle bewegt werden. Dazu muss das Programm sowohl auf den Druck als auch auf das Loslassen der Taste reagieren.
Dazu wird eine Variable taste_a benutzt, die zu Beginn des Programms auf False gesetzt wird. Sofort nach dem Drücken von A wird sie auf True gesetzt und die Taste A praktisch gesperrt. Erst das Loslassen der Taste A öffnet sie wieder.
Für die Taste B muss ein ähnlicher Programmteil eingefügt werden.
Ist alles richtig, kann der Eimer mit A und B bewegt werden.
Jetzt fehlt nur noch die Kontrolle, ob der Tropfen den Eimer berührt oder daneben gefallen ist. Dazu wird eine weitere Methode zu der Klasse sprite hinzugefügt. Die Methode beruehrt(objekt) vergleicht die x- und y-Koordinaten des eigenen Objektes (also wasser) mit den Koordinaten des übergebenen Objektes (eimer). Entsprechend wird ein True oder ein False zurückgegeben.
Im Hauptprogramm wird der Test dann ausgeführt, wenn der Tropfen unten ist. Im Beispiel gibt es eine Variable spielstand und eine Variable leben. spielstand wird zu Beginn auf 0 und leben auf 5 gesetzt.
Nun ist es dem Programmierer überlassen, wie er das Spiel weitergestaltet. Man kann die LEDs mit einbeziehen, die Wartezeit bei einem gefangenen Tropfen kleiner machen, das Spiel bei 0 Leben beenden...
Hinweis: Der Eimer wird nach dem Fangen eines Tropfens gelöscht. Er erscheint wieder, wenn er bewegt wird. Um das Verschwinden des Eimers zu vermeiden, wird noch eine Methode zeige() geschrieben. die an der aktuellen Stelle das Pixel wieder einschaltet. Die Methode eimer.zeige() muss dann an der richtigen Stelle aufgerufen werden.
Attribut | Bedeutung |
---|---|
self.__x | x-Koordinate |
self.__y | y-Koordinate |
self.__max_x | Maximalwert in x-Richtung |
self.__may_y | Maximalwert in y-Richtung |
self.__richtung | Enthält die Richtung, in der sich der Punkt beim nächsten Bewege-Befehl bewegt |
self.__h | Die Farbe (hue) von 0° bis 360° (Farbkreis) |
self.__s | Die Farbsättigung (saturation). Bei Farben immer 100, bei Grautönen 0. |
self.__l | Die relative Helligkeit (lightness). |
self.__pixel | Die eigentliche Neopixel-Instanz. |
Methode | Bedeutung |
---|---|
__init__() | Initialisierungsmethode. Die Methode wird autoamtisch aufgerufen, wenn von der Klasse eine neue Instantz erstellt wird. Der Methode müssen alle Startattribute übergeben. |
zeige_pixel() | Aus den h, s und l-Werten werden die RGB-Werte berechnet. Aus den x- und y-Werten wird die Nummer des Pixels berechnet. Das Pixel wird zum Leuchten gebracht. |
loesche() | Diese Methode wird bei allen Bewegungen genutzt. Wenn sich ein Pixel bewegt, muss er an der alten Stelle gelöscht und an der neuen Stelle angezeigt werden. Die Farbwerte des Objektes dürfen dabei nicht geändert werden. |
setze_farbe(h,s,l) | Die Attribute h, s und l werden auf die übergebenen Werte gesetzt. |
setze_x(x), setze_y(y) | Die Methoden überprüfen, ob der übergebene Wert zwischen 0 und dem jeweiligen Maximalwert liegt. Ist das der Fall, wird das aktuelle Pixel gelöscht und das x- oder y-Attribut neu gesetzt. |
x, y | Geben die aktuellen Werte der Attribute zurück. |
setze_richtung(richtung) | Setzt das Attribut für die Richtung der nächsten Bewegung. 1: rechts, 2: unten, 3: links, 4 oben |
bewege() | Es wird zuerst geprüft, ob eine Bewegung in die vorgegebene Richtung möglich ist oder ob der Rand überschritten wird. Ist eine Bewegung möglich, wird das aktuelle Pixel gelöscht, das x- oder y-Attribut geändert und das Pixel an der neuen Position angezeigt. |
Im Projekt wird eine Datei sprite.py erstellt, die alle Methoden und Attribute für das Objekt sprite enthält. Wird eine Instanz der Klasse erzeugt, müssen alle Parameter mit übergeben werden:
wasser=sprite.sprite(x_max,y_max,4,0,240,100,10,np)
x_max und y_max werden vorher auf den Wert 8 gesetzt. np ist eine Instanz der Neopixelklasse.
Durch den letzten Befehl wird der Tropfen sofort aufleuchten. Dazu mmuss aber die entsprechende Methode noch geschrieben werden.
Damit der Tropfen angezeigt werden kann, müssen die HSL-Farbe in RGB-Farben und die x- und y-Werte in die Nummer der LED in der Matrix aufgerufen werden.
Die Farbumwandlung wird in LED mit HSL-Farben ansteuern beschrieben.
Die LEDs werden auf der Matrix von oben links zeilenweise nach unten rechts durchnummeriert. Damit hat jede LED eine Nummer zwischen 0 und 63, in der ersten Zeile von 0 bis 7, in der darunter liegenden von 8 bis 15 usw. Solle die LED mit der Nummer 8 angesprochen werden, hat sie den x-Wert 0 und den y-Wert 1. Die Funktion umrechnen(x,y) gibt dann (1*8)+0=8 zurück.
Die Methode zeige_pixel(self) greift auf die Attribute des Objektes zu (self.__h....), macht daraus die RGB-Farbwerte und lässt das Pixel aufleuchten. Wenn bis hier hin alles läuft, leuchtet nur eine LED in der oberen Zeile in einem Blauton auf.
Im Hauptprogramm (main.py) sieht das dann so aus:
Im nächsten Schritt soll der Tropfen nach unten wandern. Dazu ist die Abbildung des Tropfens auf der Matrix zu löschen. Es dürfen aber noch keine Attribute des Objektes verändert werden. Das passiert dann im folgenden Teil.
Dazu wird aus den Objektvariablen self.__x und self.__y die Nummer der LED auf der Matrix bestimmt und die drei Farben ausgeschaltet (0,0,0). Das Objekt selber mit allen Attributen bleibt aber vollständig erhalten.
Die Variable self.__richtung enthält die Richtung, in der sich der Tropfen bewegt. Es gelten folgende Festlegungen
Damit der Tropfen nach unten fällt, schreibt man im Hauptprogramm
wasser.setze_richtung(2)
Die Methode bewege(self) entscheidet an Hand dem Inhalt der Variablen self.__richtung, was für einen Schritt zu tun ist. Dabei prüft sie, ob ein Rand überschritten wird. Falls das der Fall sein sollte, wird keine Bewegung ausgeführt.
Im Hauptprogramm wird der Wassertropfen erzeugt und leuchtet auf. Dann wird sofort die Richtung geändert. Damit man ihn sehen kann, wartet das Programm 300 ms und bewegt ihn eine Position weiter.
Setzt man die letzten beiden Befehle in eine unendliche Schleife, fällt der Tropfen bis nach unten und bleibt dort liegen. Da in der Methode bewege() geprüft wird, ob der Tropfen über den Rand läuft, muss man sich im Programm darum nicht mehr kümmern.
Damit der Tropfen weiter tropft, muss der y-Wert geändert werden. Der y-Wert ist ein Attribut des Objektes und darf nur über eine entsprechende Methode geändert werden. In der Methode wird als erstes geprüft, ob die Ränder überschritten werden. Ist das nicht der Fall, wird die Ansicht des Punktes auf der Matix gelöscht und der y-Wert neu gesetzt.
Da eventuell mehrere Attibute geändert werden sollen (x-Wert, Richtung...), wird der Punkt nicht neu angezeigt. Das muss im Hauptprogramm nach dem Ändern aller Attribute geändert wurden.
Im Hauptprogramm wird die unendliche Schleife entsprechend erweitert.
Aufgabe: Schreibe eine weitere Methode, die die Farbwerte neu setzt. Nenne die Methode setze_farbe... . Ändere das Hauptprogramm dann so, dass jeder fallende Tropfen einen um 20° verschobenen Farbwert hat. Achte darauf, dass man einen Farbkreis hat, nach Violett also wieder Rot kommen muss.
Die letzte Aufgabe konnte man lösen, wenn man eine neue Variable erzeugt hat, in der der aktuelle Farbwert gespeichert wird. In einer Entscheidung kann man kontollieren, ob der Inhalt der Variablen den Wert 360° nicht überschreitet. Das funktioniert, ist aber nicht nötig. Das Objekt wasser enthält ja selber ein Variable mit dem aktuellen Farbwert.
Man kann in der Klasse sprite eine weitere Methode schreiben, die alle Farbwerte zurückliefert (H, S und L). In return werden die drei Werte durch jeweils ein Komma getrennt angegeben. Damit gibt die Methode ein Tubel zurück!
Im Hauptprogramm wird der Tropfen bis nach unten bewegt. Ist er dort angekommen, wird der Variablen f das Tupel zugewiesen. Die Elemente werden wie in einer Liste angesprochen: f[0], f[1] und f[2].
Der Eimer ist ein leuchtender Punkt, der mit den Tasten A und B auf der unteren Zeile bewegt wird. Trifft der Wassertropfen den Eimer, zählt es als Punkt, ansonsten wird, beginnend von 5 Leben an, ein Leben runtergezählt. Bei 0 ist Schluss.
Eine einzige Zeile reicht aus, um den Eimer zu erstellen und anzuzeigen. Als Instanz der Klasse sprite hat das Objekt alle Attribute und Methoden erhalten.
Der Eimer soll sofort bei jedem Drücken von A oder B bewegt werden. Da er von der Klasse sprite abgeleitet wurde, ist das kein Problem. Problematisch ist die Wartezeit zwischen der Tropfenbewegung. sleep() bedeudet wirklich Schlafen und dabei kann man nun wirklich nichts machen. Der sleep()-Befehl hat in einem Spiel, wo es auf Schnelligkeit ankommt, nichts zu suchen.
Die zeitgetaktete Abfolge der Bewegung lässt sich eleganter mit der Funktion running_time() realisieren. Der Rückgabewert dieser Funktion ist die Zeit in Millisekunden seit dem Start des Programms.
Vor der dauerhaft-Schleife werden zwei Variable wartezeit und zeit festgelegt. Der sleep()-Befehl zu Beginn der dauerhaft-Schleife wird durch zwei neue Zeilen erstetzt. Die Entscheidung prüft, ob die Systemzeit größer ist also der Wert in der Variable zeit. Wenn das der Fall ist, wird die Variable zeit um den Wert der wartezeit erhöht. Dann wird der Rest (Tropfen fällt eins nach unten...) ausgeführt. Das nächste Mal wird dieser Programmteil ausgeführt, wenn die Systemzeit um die Wartezeit vorangeschritten ist.
Wenn man einfach schreibt:
sieht das zwar vernünftig aus, funktioniert aber nicht. Der Eimer flitzt bei jedem Tastendruck nach rechts oder nach links, lässt sich aber nicht vernünftig steuern.
Bei jedem Tastendruck darf nur einmal die Methode bewege() aufgerufen werden. Da aber die dauerhaft-Schleife sehr schnell durchlaufen wird, ist die Taste beim nächsten Durchlauf immer noch gedrückt und die Methode bewege() wird sehr oft aufgerufen. Nach dem Drücken einer Taste muss sie also sofort gesperrt werden und die Sperre darf erst beim Loslassen der Taste aufgehoben werden.
Das realisiert man mit zwei Variablen taste_a und taste_b, die vom Type boolean sind und vor der dauerhaft-Schleife auf false gesetzt werden. In der dauerhaft-Schleife werden die Variablen immer zusammen mit dem Tastendruck abgefragt. Im Bild ist der Programmteil für die Taste A zu sehen.
Nun muss noch geprüft werden, ob das Wasser im Eimer gelandet ist. Dazu wird eine weitere Methode geschrieben. Diese muss prüfen, ob von zwei Objekten (Wasser und Eimer) sowohl die x- als auch die y-Koordinaten übereinstimmen.
Der Methode, die sowohl vom Eimer als auch vom Wasser aufgerufen werden kann, wird das andere Objekt übergeben: wasser.beruehrt(eimer) oder eimer.beruehrt(wasser). In beiden Fällen liefert die Methode ein True zurück, wenn der Tropfen in den Eimer gefallen ist.
Bevor man nun die Entscheidung für getroffen oder nicht getroffen einfügt, muss man fragen, was in den beiden Fällen passieren soll. Man kann z.B. im Hauptprogramm zwei globale Variablen definieren:
Dann kann man direkt nach dem Beginn der dauerhaft-Schleife fragen, ob die Anzahl der Leben größer 0 ist. Wenn das der Fall ist, läuft das Spiel weiter, ansonsten wird der Spielstand angezeigt. Der Spielstand sind die gefangenen Regentropfen.
Um das Spiel zunehmend aufregender zu machen, wird bei jedem gefangenen Tropfen die Wartezeit um 10 ms kleiner. Das sollte aber nur bis zu einer Wartezeit von 150 ms gehen, ansonsten hat man kaum noch eine Chance, den Tropfen zu fangen.
Auch den Spielstart kann man noch etwas besser gestalten. Das Spiel beginnt nicht sofort, sondern wartet, bis der Spieler die aste A gedrückt hat.