Nachdem wir letztens Wirbel im Computer animiert haben, wollen wir heute ein kleines animiertes Spiel produzieren.
Das Ziel
Warnung! Computerspiele lenken vom realen Leben ab. Das kann zur Vernachlässigung der realen Beziehungen, der Gesundheit führen, und kann die Entwicklung von Kindern und Jugendlichen beeinträchtigen und gefährden. Seid verantwortlich und begrenzt die Zeit für Computer- und Handy-Spiele.
Warnung! Waffen und Kriege können verletzen und Menschen töten. Wenn diese Teil fiktionaler Spiele sind, heißt das nicht, dass sie harmlos wären. Beim Umgang mit Waffen sollten immer Vorsicht walten, und sie gehören nicht in die Hände von Kindern, Jugendlichen oder psychologisch labilen Menschen.
0. Programm aufsetzen
So wie bei den meisten Desktop Grafik-Apps, starten wir mit einem Hauptprogramm,
dass ein Fenster anlegt, in dem dann unsere Komponente dargestellt wird.
Unser Fenster soll auf Tastatureingaben reagieren, deshalb registrieren wir MyWindow als KeyListener. Dazu müssen wir 3 Methoden implementieren keyTyped, keyPressed, keyReleased. Für unsere Zwecke reicht es, in der Funktion keyPressed etwas auszuführen, die anderen beiden Funktionen können also leer bleiben.
Damit wir nicht die Shooter-Komponente hart verdrahten, habe ich eine abstrakte Klasse GameComponent eingeführt. Diese erfült 2 Zwecke. Zum einen muss sie von JComponent erben, kann also kein reines interface sein. Zum anderen definiert sie eine handvoll Methoden, die auf Tastaturereignisse reagieren. Diese werden in der Methode keyPressed aufgerufen. Zusätzlich haben wir da noch implementiert, dass man das Spiel mit <ESC> schnell beenden kann.
1 Der Shooter
Der eigentliche Dynamik geschieht in einer GameComponent, so wie wir das bisher mit einer JComponent gemacht haben:
Der Hintergrund besteht aus einer handvoll Felsen, die durch Punkte gespeichert werden und sich nicht bewegen sollen. Allerdings können die Felsen zerstört werden, sodass wir ihre Liste dynamisch machen background = mutableListOf<Point>(). Woraus besteht nun ein Punkt? Klar, x- und y-Koordinate, also
1
dataclassPoint(valx:Double,valy:Double){}
Außerdem müssen wir am Anfang eine Hand voll Felsen anlegen, etwa so:
Wenn du jetzt die fehlenden Methoden leer ergänzt – override fun shoot() {}, private fun loadBomb() {}, private fun loadShot() {} und data class Shot() {} – dann kannst du das Programm schon mal testen. Es sollte ein paar Felsen am oberen Fensterrand malen und ein “Flugzeug” (also eigentlich ein ‘A’) am unteren Rand. Wenn du die Tasten <<–> und <–>> drückst, bewegt sich das Flugzeug nach links oder rechts, umso schneller je öfter du drückst.
3.3 Schießen
Im Prinzip können wir Schüsse (engl. shot) einfach mit Folgendem erzeugen:
Das Verhalten des Flugzeugs ist etwas komisch: Es bewegt sich immer genau dann, wenn man eine der Tasten nach links oder nach rechts drückt. Stattdessen brauchen wir eher folgendes Verhalten:
Momentan gehen die Schüsse einfach durch die Felsen und verschwinden dann. Stattdessen wollen wir aber, dass sie explodieren. Das kann man etwa wie folgt erreichen:
Damit es nicht zu einfach wird zu gewinnen, wollen wir noch ab und zu Bomben auf das Flugzeug fallen lassen. Dazu nehmen wir an, dass sich in den Felsen feindliche Bombenwerfer verstecken und diese zufällig eine Bombe fallen lassen. Das kann man etwa so erreichen:
Eine Münze zu werfen heißt im Englischen flipCoin(). Für eine faire Münze ist die Wahrscheinlichkeit 50% (also 0.5). Das tatsächliche Ergebnis hängt aber vom Zufall ab, d.h. wir brauchen eine Zufallsquelle (engl. random). Diese müssen wir bei Programmstart initialisieren. Das kann man am bequemsten tun, indem man einen Zufallsgenerator anlegt (val random = Random(seed)), allerdings brauchen wir noch einen Startwert (engl. seed), weil im Computer nichts wirklich zufällig abläuft. Als recht zufälligen Startwert kann man die aktuelle Systemzeit in Millisekunden verwenden. Selbst falls jemand das Spiel immer zur gleichen Uhrzeit spielt, wird er es kaum auf die Millisekunde genau zur gleichen Zeit starten.
Offenbar ist fun Random.flipCoin(...) eine Erweiterungsfunktion, d.h. sie ist eigentlich nicht in der Klasse (oder im Interface) von Random enthalten, aber wir können sie so bequem wie eine Methode aufrufen. Entsprechend gibt es für Kollektionen (z.B. List oder Set) eine Erweiterungsfunktion (engl. extension function), die ein zufälliges Element auswählt. Allerdings sollte man sicherstellen, dass die Kollektion nicht leer ist.
4. Noch etwas Polieren
Ok, im Prinzip funktioniert das Spiel jetzt, aber einige unserer Methoden sind ziemlich lang, z.B. fun paint(g :Grapgics) hat ca. 30 Zeilen. Das können wir reduzieren, indem wir die in kleinere Teile aufteilen, die wir dann aus der paint-Methode heraus aufrufen, etwa so:
privatefunevolve(){attack()replicateRocks()propagateShots()evolveHero()repaint()}// method attack() as before
privatefunpropagateShots(){for(shotinshots.toList()){if(!shot.fly())shots.remove(shot)}}privatefunevolveHero(){moveHero()checkCollision()}privatefuncheckCollision(){if(state==State.EXPLODING){if(remaining>0)remaining-=0.1elsestate=State.GAME_OVER}else{valhero=Point(posX,posY)if(shots.any{it.isHitting(hero)})state=State.EXPLODING}}privatefunmoveHero(){posX+=vxif(posX>=1.0){posX=1.0vx=0.0}elseif(posX<-1.0){posX=-1.0vx=0.0}}
4.1 Felsen Replizieren
Im Prinzip sollte das Program noch so, wie vorher funktionieren. Allerdings ist mir noch eine Idee gekommen: Wenn der Held zu lange wartet, dann replizieren sich die Felsen, etwa so:
So, jetzt kann man das Programm auch in 6 Monaten noch gut verstehen und man kann ganz gut damit spielen.
9. Selbst Probieren
Wie sieht es aus? Funktioniert das Spiel bei dir?
Falls das Programm nicht auf Anhieb funktioniert, musst du dort schauen, wo die Fehlermeldung angezeigt wird. Verstehst du, was der Code an dieser Stelle machen soll? Worüber meckert der Compiler? Wie kann man das beheben?
9.1 Ein weiteres Level
Wenn man alle Felsen entfernt hat, dann steht da “You Win!!”, aber es passiert nichts weiteres. Vielleicht fällt dir etwas ein, was man im nächsten Level schwieriger machen kann. Auch wäre es sicherlich nett, wenn man das Level anzeigen würde. Dazu solltest du eine Eigenschaft (engl. property) in der Klasse einführen (z.B. private var level = 1) und die entsprechend in der paint()-Methode mit ausgeben (z.B. hinter den Punkten).