Hier ein grosses Tutorial für ein kleines Java Game. Die Sourcecodes habe ich allerdings weg gelassen da dies den Rahmen sprengen würde. ich denke dieses Tut ist auch so sehr interessant und man kann die einzelnen Schritte wunderbar nachvollziehen. Das Java Game Development Tutorial
Ihr müsst gewisse Kenntnisse in Java besitzen um die Beispiele zu verstehen. Ihr solltet also mit der Java Syntax, dem Schreiben von Methoden und (bei den komplexeren Spielen) auch mit der Erstellung eigener Klassen, Vererbung... vertraut sein.
Dieses Tutorial beschäftigt sich dabei hauptsächlich mit Applets, was mehrere Gründe hat. Zum einen ist das Einbinden von Bildern, Sounddateien und das Auffangen von Benutzereingaben in Applets wesentlich einfacher zu bewerkstelligen als in Konsolenanwendungen. Zum anderen werden die meisten Java Spiele sowieso als Onlinespiele ins Internet gestellt und finden auch dort wohl den meisten Zulauf! Ein weiterer Grund ist, dass aufwendigere Spiele in Java - Konsolenanwendungen sehr, sehr langsam werden, was offensichtlich an der Virtual Maschine von Java liegt. Das gilt ebenso für den Appletviewer und bedingt auch für Netscape. Interessanterweise schneidet der Internet Explorer von Microsoft bei der flüssigen Darstellung von bewegten Objekten am besten ab.
Im Folgenden wollen wir euch also mit den Grundlagen der Entwicklung von Java Spielen vertraut machen und euch auch noch einige weitere Techniken und Problemlösungen vorstellen. Am Ende jedes Kapitels findet ihr dann den SourceCode des behandelten Beispiels, sowie einen Link auf eine Seite, in der ihr das soeben programmierte Applet betrachten könnt.
Grundstruktur eines Applets
Nun, diese ist eigentlich denkbar einfach und ist für alle Java Applets gleich. Im Gegensatz zu einer Konsolenanwendung hat ein Applet keine public static void main (String [] args) - Methode. Stattdessen hat es folgende Gestallt:
// Importieren der nötigen Pakete
import java.applet.*;
import java.awt.*;
// Ableiten der Klasse FirstApplet von der Klasse Applet
public class FirstApplet extends Applet
{
// Nun sollten folgende Methoden implementiert werden
// init - Methode wird beim Laden des Applets aufgerufen
public void init() { }
// start - Methode wird nach dem Laden des Applets aufgerufen
public void start() { }
// Stop - Methode wird beim Verlassen der Seite mit dem Applet aufgerufen
public void stop() { }
// Destroy - Methode wird beim endgültigen Verlassen der Internetseite aufgerufen
public void destroy() { }
/** Paint - Methode dient dem Zeichnen von Elementen im Applet und wird z.B. bei Bewegung des Browserfensters und durch den Aufruf repaint() aufgerufen */
public void paint (Graphics g) { }
}
Um sich nun ein Applet im Browser ansehen zu können, muss das Applet in eine Internet - Seite eingebunden werden. Dies sieht im einfachsten Fall dann so aus:
<html>
<body>
<p><applet code = Main width=700 height=400>
</applet></p>
</body>
</html>
Hierbei ist besonders die Zeile <p><applet code = Main width=700 height=400> wichtig.
1. applet code = Main gibt die Klasse an, in der sich die Init - Methode befindet
2. width und height dienen der Bestimmung der Größe des Applets
Alle anderen HTML spezifischen Grundlagen könnt ihr am besten mit SelfHTML lernen
Die Bewegung eines Balles über das Applet
Zunächst wollen wir uns mit einer einfachen Animation beschäftigen. Wir wollen ein Java Applet programmieren, dass einen Kreis (Ball) von der einen Seite des Applets auf die andere bewegt. Dies ist sicherlich noch keine Großtat, aber irgendwie müssen wir ja beginnen.
Zu Beginn müssen wir wieder unsere Applet Grundstruktur implementieren, wobei wir allerdings noch zwei Dinge hinzufügen müssen. Als Erstes muss man, um eine Animation in Java verwirklichen zu können das Interface Runnable implementieren und die dazugehörige Methode run(). Wir müssen unser Applet also folgendermaßen erweitern:
import java.applet.*;
import java.awt.*;
public class BallApplet extends Applet implements Runnable
{
public void init() { }
public void start() { }
public void stop() { }
public void destroy() { }
public void run () { }
public void paint (Graphics g) { }
}
Um nun ein Graphik - Objekt zu bewegen, brauchen wir außerdem einen sogenannten Thread, den wir in der Start - Methode schaffen:
Threads
Ein Thread ist ein eigenständiges Programmfragment, das parallel zu anderen Threads (Multithreading) laufen kann. Threads werden in Java durch die Klasse Thread und das Interface Runnable sowie die dazugehörige Funktion run() implementiert , was wir im obigen Abschnitt ja schon getan haben. Wichtige Funktionen zur Erzeugung eines Threads sind:
* Thread.start(): Startet einen Thread
* Thread.stop(): Stopt einen Thread
* Thread.sleep(Zeit in Millisekunden): Stopt den Thread für die Angegebene Zeitspanne
Die weiteren Funktionen der Klasse Thread können in der Java - API nachgelesen werden.
public void start ()
{
// Schaffen eines neuen Threads, in dem das Spiel läuft
Thread th = new Thread (this);
// Starten des Threads
th.start ();
}
Diesen Thread lassen wir nun in der Run - Methode laufen. Nach jedem Durchlauf der Run - Methode stoppen wir den Thread für eine gewisse Zeit um ihn dann erneut durchlaufen zu lassen: Die run - Methode sollte also folgendermaßen aussehen:
public void run ()
{
// Erniedrigen der ThreadPriority
Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
// Solange true ist läuft der Thread weiter
while (true)
{
// Neuzeichnen des Applets
repaint();
try
{
// Stoppen des Threads für in Klammern angegebene Millisekunden
Thread.sleep (20);
}
catch (InterruptedException ex)
{
// do nothing
}
// Zurücksetzen der ThreadPriority auf Maximalwert
Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
}
}
Wir haben nun eine Endlosschleife, die alle Anweisungen innerhalb des while (true) - Blockes ausführt, 20 Millisekunden wartet um dann die selben Anweisungen erneut auszuführen. Wie aber sollen wir nun einen Kreis, der vom Applet gezeichnet wird bewegen.
Nun, der Kreis hat ja eine x - Koordinate und eine y - Koordinate. Wenn wir nun in jedem Durchlauf des Threads die x - Koordinate um eins erhöhen würden, so würde sich der Ball entlang der x - Achse, bei gleichbleibender y- Koordinate, bewegen.
Zeichnen wir also zunächst einen Kreis. Dazu verändern wir die paint - Methode wie folgt:
public void paint (Graphics g)
{
// Setzten der Zeichenfarbe auf Rot
g.setColor (Color.red);
// Zeichen eines gefüllten Kreises
g.fillOval (x_pos - radius, y_pos - radius, 2 * radius, 2 * radius);
}
und müssen noch folgende Instanzvariablen (am Kopf des Programmes) initialisieren:
int x_pos = 10;
int y_pos = 100;
int radius = 20;
Um den Ball nun zu bewegen verändern wir pro Threaddurchlauf die x -Koordinate um eins, die run - Methode sieht also jetzt so aus:
public void run ()
{
...
while (true)
{
// Verändern der X - Koordinate des Balles
x_pos ++;
...
}
}
Wenn man das Applet nun wie im 1. Kapitel gesehen in eine Html - Seite einbindet, dann sollte sich ein roter Ball einmal quer über das Applet bewegen.
Reduzierung des Bildschirmflackerns
Wie ihr in dem vorigen Beispiel sicherlich bemerkt habt, flackert der Ball sehr stark, was sich bei wirklich aufwendigen Graphiken noch verstärken würde. Der Grund dafür ist recht einfach. Vor jedem Aufruf der Methode paint() durch repaint() in unserem Thread wird das Bild vollständig gelöscht um dann erneut gezeichnet zu werden. Dadurch erscheint für den Bruchteil einer Sekunde ein vollkommen leerer Bildschirm, was wir als Flackern wahrnehmen. Um dieses Flackern des Bildes zu verhindern, gibt es zunächst drei verschiedene Möglichkeiten.
1. Bildschirm nicht löschen
2. Bildschirm nur dort löschen, wo es nötig ist
3. Die Doppelpufferung
Bildschirm nicht löschen
Dieser Ansatz erscheint auf den ersten Blick als die Lösung aller Probleme. In unserem Fall würde es aber bedeuten, dass der Ball eine dicke, rote Linie hinterlassen würde. Wir wollen den Ball aber als bewegten Ball wahrnehmen und nicht als Objekt, dass eine rote Linie hinter sich herzieht. Das Löschen des Bildschirmes zu unterdrücken ist also nur für nicht bewegte Animationen sinnvoll.
Auch für den späteren Verlauf dieser Lektion ist es wichtig zu verstehen, dass jedesmal wenn repaint() aufgerufen wird, nicht gleich auch die Methode paint() aufgerufen wird. Stattdessen ist eine Methode namens update() dazwischengeschaltet. Wird diese nicht überschrieben, so ist diese Methode dafür verantwortlich, dass der Bildschirm mit einem leeren Bild überlagert und anschließend durch den Aufruf von paint() von der Methode update() aus, neu gezeichnt wird. Um also das Löschen des Bildschirms zu unterdrücken, muss man update() so überschreiben, dass sie nur noch paint() aufruft. Dies ist in drei Zeilen zu bewerkstelligen.
public void update(Graphics g)
{
paint(g);
}
Wie gesagt ist diese Möglichkeit nur bei statischen Animationen wirklich praktikabel und wird somit nur selten verwendet.
Bildschirm nur dort löschen, wo es nötig ist
Die Idee dieses Ansatzes besteht darin, den Bildschirm nur dort zu löschen, wo sich im letzten Schritt ein Graphikelement befunden hat, sich nun aber keines mehr befindet. Dieser Ansatz lässt sich z. B. bei dem sehr bekannten Spiel Snakin' ganz gut umsetzen, indem man als letztes Glied der Schlange ein Element in der Farbe des Hintergrundes, und somit unsichtbares Element, anhängt. Dadurch wird immer beim nächsten Schritt das vorher letzte Element überlagert. Da sich diese Methode nur in bestimmten Fällen anwenden läßt und dann immer nach einer, auf das spezifische Problem abgestimmten, Vorgehensweise verlangt, möchte ich nicht näher auf diese Methode eingehen. Wenden wir uns stattdessen der sogenannten Doppelpufferung zu, die man als Standardmethode ohne Abwandlung in jedes Applet mit Animationen übernehmen und somit das Flakern des Bildes effektiv unterdrücken kann.
Die Doppelpufferung
Die Doppelpufferung kann in jedes Applet, ohne großen, programiertechnischen Aufwand eingebaut werden. Beim Doppelpuffern wird bei jedem Animationsschritt zunächst die gesamte Bildschirmausgabe in ein Offscreen-Image geschrieben. Erst wenn alle Ausgabeoperationen abgeschlossen sind, wird dieses Offscreen-Image auf die Fensteroberfläche kopiert. Im Detail sind dazu folgende Schritte erforderlich:
1. Das Fensterobjekt beschaft sich durch Aufruf von createImage ein Offscreen-Image und speichert es in einer Instanzvariablen ( = Erzeugen eines leeren Bildes)
2. Durch Aufruf von getGraphics wird ein Grafikkontext zu diesem Image beschafft
3. Alle Bildschirmausgaben (inklusive Löschen des Bildschirms) gehen zunächst auf den Offscreen-Grafikkontext ( = Zeichnen des Bildes im Hintergrund)
4. Wenn alle Ausgabeoperationen abgeschlossen sind, wird das Offscreen-Image mit drawImage in das Ausgabefenster kopiert. ( = Zeichnen des Bildes im Vordergrund)
Durch diese Vorgehensweise wird erreicht, daß das Bild komplett aufgebaut ist, bevor es angezeigt wird. Da beim anschließenden Kopieren die neuen Pixel direkt über die alten kopiert werden, erscheinen dem Betrachter nur die Teile des Bildes verändert, die auch tatsächlich geändert wurden. Ein Flackern, das entsteht, weil Flächen für einen kurzen Zeitraum gelöscht und dann wieder gefüllt werden, kann nicht mehr auftreten.
Einziger, gravierender Nachteil der Doppelpufferung ist jedoch, dass die Konstruktion des OffscreenImages ziemlich speicheraufwendig ist, und außerdem die Bilddaten zweimal geschrieben werden. In den meisten Fällen und auf schnellen Rechnern ist es jedoch weitaus vorteilhafter, in sehr kurzer Zeit die Doppelpufferung zu realisieren, als sich lange mit der Suche nach alternativen Möglichkeiten aufzuhalten.
So, aber nach der ganzen Theorie nun zur Implementierung der Doppelpufferung in unserem Ball - Applet aus dem letzten Kapitel!
Der Programcode zur Umsetzung der Doppelpufferung
// Definition zweier Instanzvariablen für die Doppelpufferung im Kopf des Programmes
private Image dbImage;
private Graphics dbg;
... anderer Programcode ...
/** Update - Methode, Realisierung der Doppelpufferung zur Reduzierung des Bildschirmflackerns */
public void update (Graphics g)
{
// Initialisierung des DoubleBuffers
if (dbImage == null)
{
dbImage = createImage (this.getSize().width, this.getSize().height);
dbg = dbImage.getGraphics ();
}
// Bildschirm im Hintergrund löschen
dbg.setColor (getBackground ());
dbg.fillRect (0, 0, this.getSize().width, this.getSize().height);
// Auf gelöschten Hintergrund Vordergrund zeichnen
dbg.setColor (getForeground());
paint (dbg);
// Nun fertig gezeichnetes Bild Offscreen auf dem richtigen Bildschirm anzeigen
g.drawImage (dbImage, 0, 0, this);
}
Wie schon erwähnt läßt sich der obige Programmcode in jedes Applet übernehmen, so auch in unser Ball - Beispiel von vorher.
Den Ball im Spielfeld halten
Nachdem wir nun einen Ball ohne Bildschirmflackern über das Spielfeld bewegen können, berherschen wir im Grunde die beiden wichtigsten Grundtechniken zum Programmieren von Spielen mit Animationen. Wir wenden uns also nun einer wichtigen Technik zu, die allerdings nicht mehr durch ein Standardverfahren zu realisieren ist, sondern auf jede Spielsituation speziel zugeschnitten sein muss (natürlich kann man, hat man die Idee einmal gehabt bzw. irgendwo gesehen, immer wieder in ähnlichen Situationen darauf zurückgreifen!). In unseren letzten beiden Programmstücken laüft der Ball lediglich einmal über das Spielfeld. Angenommen wir wollten aber verhindern dass der Ball das Spielfeld verlässt. Stattdessen soll er, hat er den Rand des Spielfeldes erreicht, einfach in die entgegengesetzte Richtung abprallen. Was also müssten wir tun? Am besten wäre es nun natürlich, wenn ihr euch dieses Problem selbst überlegen und es dann auf eure Weise lösen würdet, denn es gibt mit Sicherheit mehr als nur eine Lösung dafür. Im Folgenden erfahrt ihr meinen Lösungsansatz, als Grundlage dient wieder das Ball - Applet, diesmal in der überarbeiteten Version mit Doppelpufferung.
Ballbewegung in verschiedene x - Richtungen
Zunächst müssen wir uns klar machen, wie wir den Ball im ersten Kapitel bewegt haben. Wir haben in jedem Durchlauf des Threads die x - Position des Balles um eins erhöht und den Ball an der neuen Stelle erneut gezeichnet. Wir haben also, ohne es zu wissen, einen Geschwindigkeitsvektor in x - Richtung verwendet, immer wenn wir mit x_pos ++ die x - Position verändert haben. Daher können wir auch explizit eine Variable x_speed definieren, die die Geschwindigkeit in x - Richtung angibt und diese mit dem Aufruf von x_pos += x_speed zu x_pos hinzuzählen. Wenn x_speed dabei den Wert 1 hat, so erhöht sich die x - Koordinate des Balles bei jedem Durchlauf um den Wert 1, der Ball bewegt sich also von links nach rechts. Um die Richtung der Ballbewegung umzukehren, müssen wir der Variable x_speed nur den Wert -1 zuweisen. Somit wird die x - Koordinate des Balles in jedem Threaddurchlauf um 1 erniedrigt und der Ball bewegt sich von rechts nach links.
Nachdem wir die Richtung der Ballbewegung nun umkehren können, ist es nur noch notwendig, zu testen, wann eine Umkehrung der Richtung notwendig wird. Dies erreichen wir mit folgendem Programmstück, das wir in die run - Methode unseres Programmes einfügen.
// Wenn der Ball den rechten Rand berührt, dann prallt er ab
if (x_pos > appletsize_x - radius)
{
// Ändern der Richtung des Balles
x_speed = -1;
}
// Ball brührt linken Rand und prallt ab
else if (x_pos < radius)
{
// Ändern der Richtung des Balles
x_speed = +1;
}
Wir bestimmen also in jedem Durchlauf, ob die x - Position des Balles größer als der rechte Rand bzw. kleiner als der linke Rand wird. Ist dies der Fall, so ändern wir den Richtungsvektor x_speed des Balles und kehren somit die Bewegungsrichtung des Balles um.
Vielleicht fragt ihr euch nun, warum ich den Radius mit einbeziehe. Die x - Koordinate des Balles ist seine Mitte, also ist die rechte bzw. linke Koordinate um den Radius des Balles größer bzw. kleiner. Damit der Ball nicht erst vom Rand abprallt, wenn er den Rand schon zur Hälfte überschritten hat, sondern schon wenn er den Rand berührt, darf seine x - Position bei einer Appletgröße von 300 und einem Radius von 20 höchstens 280 bzw. 20 betragen, damit es so aussieht, als würde er direkt vom Rand zurückgeschossen.
Ball auf die andere Seite "beamen"
Wenn man das Prinzip der Abfrage nach der Position des Balles einmal verstanden hat, kann man natürlich viele andere Sachen implementieren. Um den Ball, nachdem er auf der einen Seite verschwunden ist auf der anderen wieder auftauchen zu lassen, genügt eine einzige Abfrage und diesmal die Veränderung der x - Position:
// Test ob Ball Spielfeld ganz verlassen hat if (x_pos > appletsize_x + radius)
{
// Ändern der Position des Balles
x_pos = -20;
}
Später werde ich noch auf die Bewegung eines Balles in x und y Richtung, also in der Ebene eingehen, was allerdings, hat man das Prinzip mit den Richtungsvektoren verstanden, sehr einfach ist. Man muss lediglich einen Vektor in y - Richtung einfügen und die y - Position zusammen mit der x - Position in jedem Threaddurchlauf verändern. Die Abfragen nach der Position gestallten sich ebenfalls nur geringfügig komplizierter. Wer will kann ja schon mal ein Applet programmieren, in dem der Ball nach allen Richtungen fliegen und von allen Wänden abprallen kann.
Sounddateien in Applets
Sounddateien lassen sich ziemlich einfach in ein Applet einbinden, wozu nur wenige Zeilen Code nötig sind. Ich möchte in diesem Kapitel anhand von unserem BallBounce - Applet aus dem letzten Kapitel zeigen, wie man eine Sounddatei (*.au Format, *.wav kann Java nicht lesen!!) in ein Applet lädt, und diese immer dann, wenn der Ball von der Wand abprallt, abspielen kann.
Zunächst müssen wir eine Objektreferenz auf ein AudioClip - Objekt schaffen durch die wir nach dem Laden der Audiodatei auf den Clip zugreifen können. Wir deklarieren also eine Instanzvariable vom Typ AudioClip namens bounce.
//Instanzvariable für den AudioClip bounce
AudioClip bounce;
Aus einem Grund, der mir nicht bekannt ist, liefert InternetExplorer 5 bei meinem Rechner einen Fehler, wenn diese Variable private oder public ist, daher habe ich sie ohne Zugriffsspezifikator deklariert.
Nun müssen wir die Datei in der init() - Methode (oder an einer anderen Stelle im Applet) laden (Achtung: Um diesen Befehl ausführen zu können, müssen die Klassen java.net.* und java.applet.* importiert worden sein!!). Dies kann mit folgendem Code geschehen:
// Laden einer Audiodatei, die sich im selben Verzeichniss befindet wie der compilierte Appletcode
bounce = getAudioClip (getCodeBase(), "bounce.au");
Die Audiodatei kann nun mit dem Befehl bounce.play() an jeder Stelle der Klasse abgespielt werden.
Wenn die Audiodatein in einem Unterordner liegen, der z. B. audio heißt, dann kann man die Dateien mit bounce = getAudioClip (getCodeBase(), "audio/bounce.au"); laden. Leider lassen sich, wie oben schon erwähnt keine *.wav - Dateien, die wesentlich gebräuchlicher sind als *.au - Dateien, in Applets abspielen. Man kann aber mit den meisten Wave - Editoren (z. B. die Shareware GoldWave) *.wav - Dateien in *.au - Dateien umwandeln. Es müssen jedoch zusätzlich 8 Bit, 8000 khz, Mono Dateien sein. Alles in Allem ist das Umwandeln etwas kompliziert, ihr findet aber bei vielen Java Spielen, die zum Runterladen im Netz stehen, *.au - Sounddateien, die ihr ja fürs erste mal verwenden könnt.
Der zweite Teil folgt im Forum. Bitte achtet einfach auf die Überschrift des Threats.
Danke Euer Admin
Quelle: Autoren / Fabian Birzele / Vadim Murzagalin http://www.javacooperation.gmxhome.de/