In unserem Spiel soll der Spieler zwei Bälle (einen roten und einen blauen), die sich, per Zufallsgenerator bestimmt, in verschiedene Richtungen bewegen, durch einen Mausklick auf den Ball abschießen. Läßt der Spieler einen Ball aus dem Spielfeld fliegen, ohne ihn getroffen zu haben, so verliert er eines von anfangs 10 Leben. Trifft er den Ball, so erhält er eine gewisse Zahl von Punkten (abhängig von der Geschwindigkeit des Balles) und ein neuer Ball erscheint auf dem Spielfeld. Das Spiel soll außerdem Sounddateien bei einem Schuss, bei einem Treffer und wenn ein Ball das Spielfeld verlassen hat abspielen.
In dieser Klasse sollen alle Methoden, die das Applet und die Animation direkt betreffen, implementiert werden. Dazu gehören die Methoden init(), start(), stop(), paint(), update(), run() und die mouseDown() - Methode zum Auffangen von Benutzerereignissen. Die Klasse verwaltet zudem alle für das Spiel nötigen Objekte (zwei Ball Objekte und ein Spieler Objekt) sowie den Thread.
Diese Klasse verwaltet Punktestand und Leben des Spielers. Dazu verfügt sie über die Methoden getScore() und getLives(), die die aktuellen Werte an die Main - Klasse übergeben, ebenso wie die addScore(int plus) und looseLife() - Methoden, mit denen man Leben abziehen und Punkte hinzufügen kann.
Diese Klass enthält die kompliziertesten Methoden des Spiels. Sie definiert alle wichtigen Funktionen eines Ball Objektes. Diese sind:
Bei jedem Threaddurchlauf in der Main - Methode wird die move - Methode jedes Balles aufgerufen. Diese legt die neuen Koordinaten des Balles fest und testet zudem selbstständig, durch den Aufruf der Methode isOut(), ob der Ball im Aus oder noch im Spielfeld ist. Ist der Ball im Aus, so ruft isOut() die looseLife() - Methode des Player - Objektes auf und der Spieler verliert ein Leben.
Immer wenn der Spieler in das Applet klickt, wird dieses Ereigniss von mouseDown in der Klasse Main aufgefangen und die Methode userHit () für beide Bälle aufgerufen. Diese testet, ob der Ball getroffen wurde und liefert je nach dem true oder false zurück. Ist der Rückgabewert true, so wird von der Main - Methode aus ballWasHit () aufgerufen, die alle weiteren nötigen Schritte (Ball zurücksetzen, Punkte des Spielers erhöhen...) ausführt.
Zu allererst ist es wichtig, dass ihr euch den gesammten SourceCode des Spiels (am Ende der Seite) als *.zip - Archiv runterladet und immer im Auge habt, während ich den Code erkläre. Dabei werde ich nur auf bestimmte Teile des Programmes eingehen und ihr solltet in der Lage sein bzw. es bald lernen, den restlichen Sourcecode (mit Hilfe der andernen Kapitel) zu lesen und zu verstehen um euch auf diese Art und Weise weiteres Wissen anzueignen.
Am allerwichtigsten aber ist es, dass ihr euch möglichst bald an ganz einfache, eigene Aufgaben wagt und euch überlegt wie ihr bestimmte Probleme in diesem Spiel gelöst hättet. Denn jedes Spiel erfordert ab einem gewissen Punkt ganz individuelle Lösungen, die ihr nirgendwo finden werdet und bei denen euch oft auch niemand helfen kann. Benutzt also dieses erste Spiel und die anderen Applets / Sourcecodes auf diesen bzw. anderen Seiten (siehe auch Links) als Ideeengeber und "Nachschlagewerke", mit deren Hilfe ihr eure eigenen Spiel entwickelt. Denn nur Übung macht den Meister!
Wie ich schon mal im ersten Kapitel angedeutet habe, wollen wir die beiden Bälle nun nicht mehr nur horrizontal über das Applet bewegen, sondern wir wollen beide Dimensionen (x und y - Koordinate) des Applets ausnutzen. Dazu müssen wir zu dem uns schon bekannten Richtungsvektor in x - Richtung (x_speed) noch einen weiteren in y - Richtung (y_speed) hinzufügen. Unser Ball hat also nun eine Geschwindigkeit in x - Richtung, die bei jedem Aufruf der Methode move() zur aktuellen x - Koordinate hinzugezählt wird und ebenso eine y - Geschwindigkeit, die jeweils zur y - Koordinate hinzuaddiert wird. Der y_speed soll bei beiden Bällen immer konstant bleiben. Dabei soll der eine Ball nach oben (y_speed = -1) und der andere Ball nach unten (y_speed = 1) fliegen. Man beachte, dass das Koordinatensystem von Java sozusagen auf dem Kopf steht (y - Werte werden von oben nach unten immer größer!). Des weiteren soll diese Geschwindigkeit in x - Richtung nach jedem Abschuss / Aus des Balles neu, per Zufall bestimmt werden.
Zunächst müssen wir uns also einen Zufallsgenerator in Java beschaffen. Wir importieren hierfür die Klasse java.util.*; in unsere Ballklasse und deklarieren anschließend folgendes Objekt als Instanzvariable der Klasse Ball:
Nun können wir mit dem Befehl rnd.nextInt() eine zufällige Integerzahl erzeugen. Da wir eine Zahl zwischen -3 und +3 erzeugen wollen, müssen wir diese Zahl anschließend noch modulo 4 rechnen um nur Zahlen in diesem Zahlenraum zu erhalten.
Nun können wir immer wenn ballWasHit() aufgerufen wird oder wenn isOut() den Wert true zurückliefert die neue Flugrichtung des Balles zufällig bestimmen, indem wir x_speed den zufallsgenerierten Wert zuweisen.
Nachdem wir die Instanzvariablen x_speed und y_speed deklariert und im Konstruktor initialisiert haben, können wir sie in jedem Aufruf von move() zu den aktuellen Koordinaten hinzuzählen. x_speed wird dabei immer wieder neu bestimmt (siehe oben), y_speed bleibt konstant. Die move() - Methode sieht also folgendermaßen aus:
Die Methode isOut() prüft, ob ein Ball eine der Grenzen des Spielfeldes überschritten hat. Trifft eine der vier Abfragen zu, so wird der Ball auf seine Startposition zurückgesetzt, eine Audiodatei abgespielt, die Flugrichtung neu bestimmt und der Spieler verliert ein Leben (wie genau, das folgt später noch). Das Prinzip ist das gleiche wie schon in unserem dritten Applet (BallReverse) besprochen, mit dem einzigen Unterschied, dass wir hier vier Grenzen haben, die wir nach möglichen Überschreitungen abfragen müssen und nicht nur zwei wie in unserem Beispiel aus Kapitel 1 c. Daher möchte ich nicht näher auf die programmiertechnische Seite dieser Methode eingehen, versucht stattdessen die einzelen Schritte (im Grunde ist es nur einer, der sich viermal wiederholt) alleine nachzuvollziehen (mit den gegebenen Kommentaren und dem Kapitel 1c sollte das kein Problem sein).
Dieses Problem ist wohl das Schwierigste in diesem Spiel. Der Ball hat ja eine x und eine y - Koordinate. Klickt der Spieler auf das Applet, so werden die x und die y - Koordinate mit an die aufgerufenen Methode userHit (int x, int y) übergeben. Nun muss die Methode entscheiden, ob der Schuss den Ball getroffen hat, oder nicht. Wie soll man das bewerkstelligen.
Es genügt nicht nur x und y - Koordinate des Balles mit denen des Schusses zu vergleichen (bei Gleichheit gilt der Ball dann als getroffen), denn dann müsste der Spieler immer genau die Mitte des noch dazu bewegten Balles treffen. Eine fast unlösbare Aufgabe!
Also, zweite Idee: Man akzeptiert alle Koordinaten als Treffer, die um einen gewissen Betrag kleiner bzw. größer sind als die Koordinaten des Balles. Dies muss natürlich für die x und die y - Koordinate gleichermaßen zutreffen (&& - Abfrage). Auf diese Weise testet man, ob der Klick des Spielers in einem Quadrat um den Ball (bei Radius 10 z. B. mit Kantenlänge 20) erfolgt ist. Diese Idee hatte ich bei meinem ersten Versuch dieses Spiel umzusetzen implementiert, mit leider nur mäßigem Erfolg. Das Ganze hat mal funktioniert, mal wieder nicht, obwohl der Ball richtig getroffen wurde. Der Grund dafür ist mir immernoch schleierhaft.
Nun denn, die dritte Idee: Aus der Schule dürfte den meisten noch der Begriff eines Vektors geläufig sein. Wir haben es hier nun zwei Vektoren zu tun, die bei einem Schuss entstehen: Einem Schussvektor (x_maus, y_maus) des Mausklicks und einem Positionsvektor des Balles (x_ball, y_ball). Wenn wir nun die Länge des Verbindungsvektors dieser beiden Vektoren bestimmen und dieser kleiner als der Radius des Balles ist, so haben wir den Ball getroffen.
Zunächst berechnen wir also den Verbindungsvektor indem wir die Koordinaten des einen Vektors von denen des Anderen abziehen:
Nun können wir mit Hilfe des Skalarproduktes bzw. Pythagoras (c = Wurzel aus a² + b²) die Länge dieses Verbindungsvektors bestimmen:
Als letzten Schritt testen wir, ob die Länge dieses Vektors kleiner ist als ein gewisser Betrag, bis zu dem wir den Ball noch als getroffen ansehen. Ich habe für diese Zahl 15 gewählt obwohl der Ball nur einen Radius von 10 hat. Aber nachdem ich einige Werte getestet habe, erschien mir dieser Wert als der am besten taugliche.
Damit können wir den Ball abschießen, wenn wir auf bzw. kurz daneben schießen. Die Rückgabewerte ergeben sich aus der oben beschriebenen Struktur und Zusammenarbeit der Methoden.
Um dies zu verwirklichen haben wir die Player - Klasse mit den Methoden looseLife() und addScore (int plus) ausgestattet. Immer wenn ein Ball das Spielfeld verlässt, ruft er player.looseLife() in der Methode isOut() auf. Dadurch verliert der Spieler ein Leben. Trifft der Spieler den Ball (ober beschrieben), so ruft die Methode userHit() player.addScore (10* Math.abs(x_speed) + 10) auf und zählt somit je nach Geschwindikeit des Balles Punkte zu denen des Spielers hinzu. Umso schneller ein Ball fliegt, umso mehr Punkte kann man durch einen Abschuss somit erreichen. Die Methoden addScore und looseLife sind dabei wirklich denkbar einfach.
Einziger Knackpunkt: Man muss sehen, dass das Player - Objekt in der Klasse Main geschaffen wird. Die Refferenz auf das Spieler - Objekt muss also im Konstruktor oder in der Methode (userHit() bzw. isOut()) die Methoden des Objektes verwendet, an die Klasse Ball übergeben worden sein!
Dies ist eine wichtige Technik, denn es kommt häufig vor, dass mehrere Klassen Zugriff auf ein und das selbe Objekt benötigen. Man initialisiert das Objekt dann einfach z. B. in der Klasse Main (die ich gerne verwende um alle Objekte des Spiels zu verwalten) und übergibt die Refferenz von dort aus an alle anderen Objekte, die die Refferenz benötigen.
Natürlich passt zu unserem Spiel ein Fadenkreuz als Mauszeiger viel besser, als ein normaler Zeiger. Um uns einen Fadenkreuzmauszeiger zu beschaffen, müssen wir unserer Main - Klasse lediglich drei Codezeilen hinzufügen: Zunächst eine Instanzvariable der Klasse Cursor:
In die Init - Methode fügen wir dann folgende Zeilen ein:
Weitere Klassenvariablen der Klasse Cursor und somit weitere Mauscursor, können in der API nachgelesen werden.
Bis auf eine kleine Kleinigkeit ist unser Spiel nun fertig: Es ist für den Spieler sehr unangenehm, wenn das Spiel schon losgeht, bevor er sich überhaupt die Anleitung durchgelesen hat. Wir wollen das Spiel daher erst dann starten, wenn er einen Doppelklick in das Appletfenster ausgeführt hat. Das hat nicht nur den Vorteil, dass der Spieler den Startpunkt des Spieles selbst bestimmen kann, sondern wenn man in seinem Spiel Tastaturereignisse verwendet, dann kann man den Tastaturfocus für das Applet (wird durch Klick auf das Applet erreicht) auf diese Weise sicherstellen. Des weiteren soll das Spiel natürlich dann beendet sein, wenn der Spieler alle seine Leben verspielt hat.
Zunächst fügen wir also in die Klasse Main eine boolsche Instanzvariable mit Namen isStoped ein. Ist sie true, so ist das Spiel gestoppt, ist sie false, so läuft das Spiel. Nun fügen wir in unseren Thread (run - Methode) eine Abfrage ein, die die Methoden zur Bewegung des Balles nur dann aufruft, wenn der Spieler mehr als 0 Leben hat und gleichzeitig isStoped false ist. repaint() wird weiterhin immer nach dem Starten des Threads aufgerufen.
Im nächsten Schritt wollen wir in der paint() - Methode eine weitere Abfrage einfügen. Sie soll, solange der Spieler noch Leben hat, die Bälle sowie die Angabe über Leben und Punkte zeichen. Ist das Spiel gestoppt, so zeichnet sie zudem die Information, dass das Spiel mit einem Doppelklick gestartet werden kann, auf den Bildschirm. Hat der Spieler keine Leben mehr, so wertet die paint() - Methode seine Punktezahl aus, schreibt eine Bewertung der Ergebnisse auf den Bildschirm, sowie weitere Informationen auf den Bildschirm und gewährleistet, dass das Spiel nach einem weiteren Doppelklick des Spielers wieder gestartet wird. Die paint() - Methode gestalltet sich also folgendermaßen:
Nun ist es fast geschafft! Im Moment haben wir aber noch keine Kontrolle darüber, wann wir das Spiel starten, da wir in die MouseDown - Methode noch keine Befehle eingefügt haben, die das Umschalten von dem gestoppten Spielzustand in das laufende Spiel erlauben. Um dies zu erreichen müssen wir die mouseDown() - Methode folgendermaßen verändern:
So, nun habt ihr es hinter euch und wenn ihr auch die Teile, die ich nicht so ausführlich besprochen habe, verstanden habt, dann steht eurem ersten Spiel ja nichts mehr im Wege.
Die nächsten Kapitel werden sich noch mit einigen weiterführenden Problemen und Lösungen befassen, die ich in meinen bisher programmierten Spielen verwendet bzw. irgendwo mal gesehen habe.
Ansonsten entlasse ich euch hiermit "in die Freiheit", in der Dank der Mächtigkeit und Flexibilität von Java beinahe alles möglich ist, wünsche euch viel Spaß und Erfolg bei der Entwicklung eurer eigenen Spiele, hoffe, dass ich euch in diesem Tutorial ein wenig weiterhelfen konnte und wenn ihr eure ersten Spiele geschrieben, Wünsche, Anregungen, Beschwerden, eigene Tutorials zu bestimmten Themen... oder ein Problem habt, dann schreibt bzw. schickt mir doch einfach eine Mail bzw. euer Spiel...!
Zuguterletzt könnt ihr euch, wie immer den SourceCode runterladen und das Spiel ausprobieren.
SourceCode download (*.zip - Datei)
Applet ansehen