Inhalt Virtuelle Lehrerfortbildung im Fach Informatik in Niedersachsen © Hans-Georg Beckmann 2004 Threads 1. Einleitung.................................................................................1 2. Die Konstuktion von Threads...............................................1 3 Ein einfaches Beispiel.............................................................3 3.1 Laufzeitprobleme ...................................................................8 3.2 Synchronisationsprobleme ....................................................9 4. Ein weiteres Beispielprogramm..........................................10 5 Eine Animation .....................................................................12 5.1 Aufgaben................................................................................18 6 Das Spiel Pong - ein Programmgerüst...............................19 6.1 Clipping .................................................................................22 6.2 Keyevents...............................................................................23 6.3 Aufgaben................................................................................24 Seite 1 Threads Virtuelle Lehrerfortbildung im Fach Informatik in Niedersachsen © Hans-Georg Beckmann 2004 Was der Mensch über Threads wissen sollte Im Normalfall haben wir es mit Programmen zu tun, in denen die Anweisungen einen "Anweisungsstrang" bilden, in dem die einzelnen Befehle nacheinader abgearbeitet werden. Java bietet die Möglichkeit, mehrere von diesen Strängen ( Threads ) parallel ablaufen zu lassen, was z.B. für animierte Grafiken von besonderm Vorteil sein kann, wie wir an einigen Beispielen sehen werden oder schon gesehen haben. Threads kann man in Java entweder aus der Klasse THREAD ableiten oder das Interface RUNABLE verwenden. In beiden Fällen muss der Befehlscode, der parallel zu einem anderen Befehlscode ausgeführt werden soll, in einer Methode r u n angegeben werden. Die Konstruktion von Threads - start, stop, run und interrupt Ein neuer Thread wird mit der Methode s t a r t in Gang gesetzt. Ist s t a r t abgearbeitet, wird selbsttätig die Methode run aufgerufen. (Man sollte aus dem Applet oder dem Javaprogramm nie run selbst aufrufen.) Im Normalfall wird ein Thread dadurch automatisch beendet, dass die Methode run fertig abgearbeitet ist. Manchmal jedoch ist eine Benutzung der Methode stop nötig, mit der man den Thread von außen beenden kann. Auch intern kann man einen Thread beenden, wenn man die Methode public void interrupt() benutzt, die dem Thread signalisiert, dass unterbrochen werden soll. Das wird in einem Flag festgehalten, den man mit der Methode public boolean isInterrupted() abfragen kann. Weiterhin kann man einen Thread dazu bringen, ein wenig schlafen zu gehen. Das geht mit der Methode public static void sleep(long Millisekunden) die dafür sorgt, dass der Thread eine bestimmte Anzahl von Millisekunden anhält. Nun kann aber ein anderer Thread einen Weckruf an den Schlafenden schicken und ihn vor Beendigung der Ruhezeit wecken wollen. Das packt Java nicht. Es ist eine Fehlerquelle, die immer mit einer try ... catch Anweisung abgefangen werden muss. Diese sieht immer wie folgt aus: try { threadname.sleep(Anzahlmillisekunden); } catch (InterruptException e) {} Das wollen wir an einem einfachen Beispiel probieren. Seite 2 Ein einfaches Beispiel Virtuelle Lehrerfortbildung im Fach Informatik in Niedersachsen © Hans-Georg Beckmann 2004 Ein Applet soll parallel am Bildschirm folgendes tun: a) Ein Zähler läuft von 1 bis 1000 und gibt seinen Zählerstand am Bildschirm aus. Wenn wir davon ausgehen, dass das flott geht, können wir überlegen, ob wir mit sleep für eine gewisse Verzögerung sorgen wollen. b) Ein Rechteck soll mehrfach mit jeweils einer neuen Zufallsfarbe am Bildschirm gemalt werden. c) In einem Rechteck soll ein Moiremuster gezeichnet werden. Jeder sieht sofort , dass das ein Programm ist, das wir schon immer mal haben wollten. Was dabei neu ist, ist das Moiremuster. Wie geht das ?? Verraten wir jetzt ! Ein Rechteck am Bildschirm habe die Koordinaten (xEcke,yEcke) für die obere linke Ecke und (xEcke+breit,yEcke+breit) für die untere rechte Ecke. Damit hat der Mittelpunkt des Bildschirms die Koordinaten ((int)(xEcke+breit/2),(int)(yEcke+breit/2)). Die Typkonvertierung mit dem (int) erfolgt nur für den Fall, dass breit/2 keine ganze Zahl ergibt. Nun wird eine Linie gezogen von der Mitte zu (xEcke,yEcke), dann eine von der Mitte zu (xEcke + dx ,yEcke), dann zum Punkt (xEcke+ 2*dx,yEcke) usw., wobei dx eine kleine Zahl ist, die angibt, um wieviele Punkte man in x- Richtung vorankommen will. dy wird dann den Fortschritt in y- Richtung angeben. Es entsteht eine Art Fächer, der ein Moiremuster am Bildschirm hinterläßt. Setze Zähler i auf 0 solange (xEcke + i*dx <= xEcke + breit) Zeichne Linie von Mitte zum Randpunkt (xEcke+i*dx,yEcke) erhöhe i um 1 Setze Zähler i auf 0 solange (yEcke + i*dy <= yEcke + breit) Zeichne von Mitte zum Randpkt (xEcke+breit,yEcke+i*dy) erhöhe i um 1 Setze Zähler i auf 0 solange (xEcke + i*dx <= xEcke + breit) Zeichne von Mitte zum Randpunkt (xEcke+i*dx,yEcke+breit) erhöhe i um 1 Setze Zähler i auf 0 solange (yEcke + i*dy <= yEcke + breit) Zeichne von Mitte zum Randpunkt (xEcke,yEcke+i*dy) erhöhe i um 1 Die entsprechenden Zeilen im Javaprogramm sehen wie folgt aus: i=0; while(xEcke+i*dx <= xEcke + breit) { myOffScreen.drawLine(xMitte,yMitte,xEcke+i*dx,yEcke); // obere Kante i++; Seite 3 Ein einfaches Beispiel Virtuelle Lehrerfortbildung im Fach Informatik in Niedersachsen © Hans-Georg Beckmann 2004 } i=0; while(yEcke+i*dy <= yEcke + breit) { myOffScreen.drawLine(xMitte,yMitte,xEcke+breit,yEcke+i*dy); // rechte Kante i++; } i=0; while(xEcke+i*dx <= xEcke + breit) { myOffScreen.drawLine(xMitte,yMitte,xEcke+i*dx,yEcke+breit); // untere Kante i++; } i=0; while(yEcke+i*dy <= yEcke + breit) { myOffScreen.drawLine(xMitte,yMitte,xEcke,yEcke+i*dy); // linke Kante i++; } Die entsprechenden Variablen müssen natürlich vorher deklariert werden. Dabei ist myOffScreen ein Image für das Doublebuffering, wie wir es schon mehrfach verwendet haben. Die Zählmaschine ( der Thread zu Punkt a ) geht wie folgt: for (i=1;i<1001;i++) { myOffScreen.drawString("Zähler : "+Integer.toString(i);100,30); } wobei Integer.toString(i) die Zahl i in ein Stück String verwandelt Das Rechteck mit der wechselnden Farbe ( der Thread zum Punkt b ) ist: for (i=1 ;i<41;i++) { farbe = new Color((int)(Math.round(Math.random()*255)), (int)(Math.round(Math.random()*255)), (int)(Math.round(Math.random()*255))); myOffScreen.setColor(farbe); myOffScreen.paintRect(20,200,80,280); } Nun bauen wir das alles in einem Applet zusammen. Dabei wird wieder das Verfahren des Doublebuffering angewendet, dass zuerst die Bilder in den Hintergrund malt und dann mit paint sichtbar macht. Das sieht in dieser einfachen Fassung noch nicht besonders gut aus, wird aber in den nächsten Programmen immer besser werden. Hierzu schaue man sich das Threadbälleprogramm von E. Modrow auch noch einmal genau an. Nun aber das Listing: Seite 4 Ein einfaches Beispiel Virtuelle Lehrerfortbildung im Fach Informatik in Niedersachsen © Hans-Georg Beckmann 2004 import java.awt.*; import java.applet.Applet; import java.lang.*; public class Threadtest2 extends Applet { Moire myMoire; // eine Instanz der Klasse Moire ( siehe unten ) Recht myRechteck; // eine Instanz der Klasse Recheck Count myCounter; // der Zähler Image myoffScreenImage; // Bild im OffScreen zum Malen Graphics myoffScreenGraphics; // Graphicobjekt für Bild im OffScreen Dimension myoffScreenDim; // Dimensionen des OffScreenbildes public void init() { myMoire = new Moire(); myRechteck = new Recht(); myCounter = new Count(); myoffScreenDim = getSize(); // Hole Dimension aus HTML/Appletwindow myoffScreenImage = createImage(myoffScreenDim.width,myoffScreenDim.height); // erzeuge OffScrennImage mit createImage myoffScreenGraphics = myoffScreenImage.getGraphics(); // füge ein Graphicsobjekt hinzu myMoire.start(); // Methode start für alle Threads aufrufen myCounter.start(); myRechteck.start(); } // Ende von Init, alle Threads erzeugt und gestartet public void paint(Graphics g) { g.drawImage(myoffScreenImage,0,0,this); repaint(10); // *** Anmerkung beachten ! } public void update(Graphics g) { paint(g); } public void stop() { myRechteck.interrupt(); myRechteck.stop(); myCounter.interrupt(); myCounter.stop(); myMoire.interrupt(); myMoire.stop(); } *** Hinweis: Die Methode repaint mit einer Zahlenangabe für die Verzögerung funktioniert zur Zeit nicht, wenn die Javamaschine von SUN implementiert ist. ( März 2004 ). Da wir hoffen, dass dieser Fehler im nächsten Update behoben wird, bleibt die Zeile im Programmlistimng stehen. Seite 5 Ein einfaches Beispiel Virtuelle Lehrerfortbildung im Fach Informatik in Niedersachsen © Hans-Georg Beckmann 2004 //********************** jetzt die Klassen ***************** class Moire extends Thread { int xEcke=80; int yEcke=80; int breit= 200; int dx=2; int dy= 3; int i,j,k; // Zähler int xMitte,yMitte; Graphics g; Color farbe; public void run() // jeder Thread braucht sein eigenes run { while(true) { farbe = new Color((int)(Math.round(Math.random()*255)), (int)(Math.round(Math.random()*255)), (int)(Math.round(Math.random()*255))); //Farbe myoffScreenGraphics.setColor(farbe); if(isInterrupted()) break; xMitte=(int)(xEcke+breit/2); // Mitte rechnen, könnte auch yMitte=(int)(yEcke+breit/2); // globale Grösse sein i=0; while(xEcke+i*dx <= xEcke + breit) { myoffScreenGraphics.drawLine(xMitte,yMitte,xEcke+i*dx,yEcke); // oben i++; for(k=0;k<10000;k++){}; // Kurze Pause/Verzögerung } i=0; while(yEcke+i*dy <= yEcke + breit) { myoffScreenGraphics.drawLine(xMitte,yMitte,xEcke+breit,yEcke+i*dy); i++;for(k=0;k<10000;k++){}; // Kurze Pause/Verzögerung } i=0; while(xEcke+i*dx <= xEcke + breit) { myoffScreenGraphics.drawLine(xMitte,yMitte,xEcke+i*dx,yEcke+breit); i++;for(k=0;k<10000;k++){}; // Kurze Pause/Verzögerung } i=0; while(yEcke+i*dy <= yEcke + breit) { myoffScreenGraphics.drawLine(xMitte,yMitte,xEcke,yEcke+i*dy); i++;for(k=0;k<10000;k++){}; // Kurze Pause/Verzögerung } try // Thread schlafen lassen {Thread.sleep(1000);} catch(InterruptedException e){} } // Ende von while } // Ende von run } // Ende von Moire Seite 6 Ein einfaches Beispiel Virtuelle Lehrerfortbildung im Fach Informatik in Niedersachsen © Hans-Georg Beckmann 2004 class Recht extends Thread { int i,j; Color farbe; Graphics g; public void run() { while(true) { if(isInterrupted()) break; farbe = new Color((int)(Math.round(Math.random()*255)), (int)(Math.round(Math.random()*255)), (int)(Math.round(Math.random()*255))); myoffScreenGraphics.setColor(Color.white); myoffScreenGraphics.fillRect(400,200,480,300); myoffScreenGraphics.setColor(farbe); myoffScreenGraphics.fillRect(400,200,480,300); try {Thread.sleep(1000);} catch(InterruptedException e){} } // Ende von while } // Ende von run } // Ende von Recht class Count extends Thread { int i; Graphics g; public void run() { while(true) { if(isInterrupted()) break; try {Thread.sleep(50);} catch(InterruptedException e){} myoffScreenGraphics.setColor(Color.white); myoffScreenGraphics.fillRect(100,10,300,35); // freimachen myoffScreenGraphics.setColor(Color.black); myoffScreenGraphics.drawString("Zähler : "+Integer.toString(i++),100,30); // draufschreiben try {Thread.sleep(500);} catch(InterruptedException e){} } // Ende von while } // Ende von run } // Ende von Count } // Ende von Applet Probieren sie das Programm und beobachten, wie die einzelnen Teile nebeneinander abgearbeitet werden. Seite 7 Laufzeitprobleme Virtuelle Lehrerfortbildung im Fach Informatik in Niedersachsen © Hans-Georg Beckmann 2004 Laufzeitprobleme: Die oben vorgestellte Lösung wirft mit dem Entwicklungssystem JBuilder von Borland einige Probleme auf, die hier angesprochen werden müssen, obwohl uns systemspezifische Feinheiten eigentlich nicht interessieren sollten. Gibt man den obigen Programmtext so im JBuilder ein, wird das Programm ohne Fehlermeldungen übersetzt und erzeugt dann unter JBulider 9 nach dem Start Laufzeitfehler ( NullPointerException). Das liegt an den OffScreenImages, mit denen hier gearbeitet wird. Der VLIN - Kollege Stefan Bartels schreibt dazu: "....bei einigen Programmen aus dem Skript gab es einen Laufzeitfehler, wenn das Applet im Applet-Viewer vom JBuilder ausgeführt wurde. Das war dann der Fall, wenn Hintergrundbilder (Stichwort "Double-Buffering" bzw. "Doppelpufferung") mithilfe der Zeilen Image im; Graphics bg; im = createImage(breite,hoehe); bg = im.getGraphics(); verwendet werden sollten. Lösung des Problems: Man zwingt den JBuilder, direkt die HTML-Datei und nicht das Applet solo anzuzeigen. Dazu wählt man den Menüpunkt "Projekt | Projekteigenschaften...", Register "Laufzeit", "Bearbeiten", Register "Start" und dort nicht die "Hauptklasse", sondern die "HTML-Datei". Als HTML-Datei muss dann die zugehörige HTML-Datei aus dem Unterverzeichnis "Java" des Projekts ausgewählt werden. Herzliche Grüße, Stefan Bartels " Bei diesem Verfahren kann es noch passieren, dass man die gewünschte HTML-Datei nicht finden kann. Dann reicht ein Doppelklick auf die HTML-Datei im Projektfentser und schon klappt es - hoffentlich. Seite 8 Synchronisationsprobleme Virtuelle Lehrerfortbildung im Fach Informatik in Niedersachsen © Hans-Georg Beckmann 2004 Applet über HTML-Datei im JBulider gestartet Applet im Explorer gestartet Das Vorgehen ist eine "Notlösung". Es ist schon sehr erstaunlich, dass die virtuelle Javamaschine von SUN solches Verhalten zeigt. Wenn man etwa das fertige Applet im Internetexplorer von MIcrosoft laufen lässt, in den die Laufzeitumgebung von SUN eingebaut wurde, dann gibt es keine Probleme. Sogar, wenn man das Programm auf dem Apple Macintosh erzeugt, die HTML-Datei und die Klassendateien auf den PC übeträgt und wieder im Explorer mit SUN Maschine laufen lässt, klappt es. Nur im JBuilder selbst macht es Probleme. Was will uns das sagen ? Es gibt natürlich noch einen weiteren Weg aus den Schwierigkeiten: Man verzichtet auf die Hintergrundgrafik. Synchronisationsprobleme Man bemerkt beim Programmablauf ein Problem, dass immer dann auftaucht, wenn Threads gemeinsam auf eine Variable ( hier auf das OffScreenImage) zugreifen und dabei unterschiedlich lange brauchen. Wenn dann der Thread vom System unterbrochen wird, bevor er fertig ist, gibt das Synchronisationsprobleme, die zu unerwarteten Ergebnissen führen können. Java hat dafür ein Schlüsselwort bereitgestellt - s y n c h r o n i z e d - mit dem man Methoden oder Variablen solange schützen kann, bis der Thread mit seiner Arbeit fertig ist. Erst danach steht die Methode oder die Variable für den Zugriff durch andere Threads wieder zur Verfügung. Ein einfaches Beispiel, das auf dem vorangegangenen Programm basiert zeigt das nun: Seite 9 Ein weiteres Beispielprogramm Virtuelle Lehrerfortbildung im Fach Informatik in Niedersachsen © Hans-Georg Beckmann 2004 /* Ein Thraedtest, der zeigt, welche Synchronisationsprobleme auftreten können wenn man nicht beachtet, dass möglicherweise mehrer Threads auf dieselbe Variable zugreifen nach einem Beispiel aus "Goto Java" geändert und komentiert von H.-G.Beckmann 12 /01 */ import java.awt.*; import java.applet.Applet; import java.lang.*; public class Threadtest4 extends Applet { int zahl=0; // Count myCounter1,myCounter2; // Image myoffScreenImage; // Graphics myoffScreenGraphics; // Dimension myoffScreenDim; // da greifen beide drauf zu die Zähler Bild im OffScreen zum Malen Graphicsobjekt für Bild im OffScreen Dimensionen des OffScreenbildes public void init() { myCounter1 = new Count(); myCounter2 = new Count(); myoffScreenDim = getSize(); //die nächsten drei Zeilen, wie gehabt myoffScreenImage = createImage(myoffScreenDim.width,myoffScreenDim.height); myoffScreenGraphics = myoffScreenImage.getGraphics(); myCounter1.start(); myCounter2.start(); // Ende von Init, alle Threads erzeugt und gestartet } public void paint(Graphics g) // wie schon gesehen { g.drawImage(myoffScreenImage,0,0,this); // zeichnet das OffScreenImage repaint(10); } public void update(Graphics g) { paint(g); } // auch bekannt public void stop() // auch bekannt { myCounter1.interrupt(); myCounter1.stop(); myCounter2.interrupt(); myCounter2.stop(); } //********************** jetzt die Klasse ***************** class Count extends Thread { Graphics g; Seite 10 Ein weiteres Beispielprogramm Virtuelle Lehrerfortbildung im Fach Informatik in Niedersachsen © Hans-Georg Beckmann 2004 public void run() { while(true) { if(isInterrupted()) break; { myoffScreenGraphics.clearRect(100,10,300,40); myoffScreenGraphics.setColor(Color.black); myoffScreenGraphics.drawString("Zähler : "+Integer.toString(zahl++),100,30); } try {Thread.sleep(500);} catch(InterruptedException e){} } // Ende von while } // Ende von run }// Ende von Count } //Ende von Applet Beim Programmlauf sieht man, dass der Zähler springt. Es werden immer wieder Zahlen ausgelassen. Eine einfache Änderung in der run-Methode macht das Programm fehlerfrei . Ändern sie die Methode wie folgt um: public void run() { while(true) { synchronized(getClass()) { if(isInterrupted()) break; { myoffScreenGraphics.clearRect(100,10,300,40); myoffScreenGraphics.setColor(Color.black); myoffScreenGraphics.drawString("Zähler : "+Integer.toString(zahl++),100,30); } try {Thread.sleep(500);} catch(InterruptedException e){} } // Ende von synchronized } // Ende von while } // Ende von run Nun zählt es korrekt. getClass() besorgt die aktuelle Klasse und die wird dann mit synchronized solange geschützt, wie nötig. Aufgabe: Bauen sie verschieden lange Pausen in den einzelnen run-Methoden des ersten Programms ein. Zum einen können sie in den "leeren" Wiederholschleifen die Endwerte verändern und zum anderen die Millisekundenangaben in den sleep-Methoden. Testen sie auch was passiert, wenn in den sleep-Methoden der Wert 0 eingestellt wird. Seite 11 Eine Animation Virtuelle Lehrerfortbildung im Fach Informatik in Niedersachsen © Hans-Georg Beckmann 2004 Ein weiteres Beispiel mit kleinen Bildchen Gern werden Threads benutzt, um Animationen zu erzeugen. Das Threadbälleprogramm hat das schon gezeigt. Im Folgenden werden kleine Bilder geladen und dann in Bewegung gesetzt. Das Programmlisting unterscheidet sich ein wenig von dem obigen Beispiel. Diesmal wird das Interface r u n a b l e benutzt. Es gibt zwar einen Unterschied zu der oben gezeigten Verfahrensweise ( Klasse mit extends Thread ) für uns ist der jedoch nicht von großer Wichtigkeit. Es zeigt aber, dass Einiges im Programmtext etwas einfacher aussieht. Vorarbeiten: Sie brauchen 11 Bilder im GIF-Format. Dieses Grafikformat sollte jedes einfache Malprogramm beherrschen. GIF unterstützt auch Transparenz, wenn es mit der Option GIF89 abgespeichert wird. Allerdings ist die Transparenz nicht wichtig. Ein Bild darf sich von den anderen unterscheiden. Es hießt in diesem Beispiel "eki.gif" Die anderen sollten die Namen Image1.gif .... Image10.gif heißen. Die Bilder müssen sich dann im gleichen Verzeichnis befinden wie die HTML-Datei und die Javaklasse. Wenn alles soweit vorbereitet ist, dann kann es los gehen. // // // // // SpriteDemo Version 2 nach einem Programm von Anatoly Goroshnik aus dem Buch Java 2 für Doofe mit Bildern von HGB 11/2001. Es werden kleine Bildchen sog. Sprites verwendet,außerdem Threads und Arrays ausführlich komentiert. In vielen Teilen geändert H.-G.Beckmann 4.4.2004 import java.awt.*; import java.applet.*; import java.lang.*; // das Übliche zu Beginn // in java.lang ist die Klasse TREAD enthalten. public class Sprite2 extends Applet implements Runnable { final int pause = 10; // für sleep final int numSprites = 11; // Anzahl der Sprites final boolean showTraces = false; // Spur am Bildschirm zeigen ja/nein Seite 12 Eine Animation Virtuelle Lehrerfortbildung im Fach Informatik in Niedersachsen © Hans-Georg Beckmann 2004 Hier wird nun in der Deklaration des Applets schon implements Runable angegeben. Kleine Bilder hießen früher Sprites, daher dies Bezeichnung. Es hat nichts mit dem gleichnamigen Getränk zu tun. Sind Variablen als final deklariert, dann können sie im Programmablauf nicht verändert werden. Das macht alles ein ganz klein wenig schneller. Image Graphics Dimension offScreenImage = null; offScreenGraphics = null; offScreenSize = null; // OffScreen; mit null initialisiert // OffScreen // OffScreengraphics- wie gehabt Da runable implementiert wurde, kann nun eine Variable der Klasse Thread vereinbart werden. Die Klasse heißt hier demo könnte aber auch z.B. Animation heissen. Thread demo; // java.lang bietet die Klasse Thread, demo ist // eine Instanz dieser Klasse Nun kommt ein Array mit Sprites, die Klasse Sprite wird noch definiert, der Array heißt demoSprites. Man sieht hier schon einen Unterschied zum obigen Programm.Die Sprites sind Instanzen einer Klasse, die nicht auch gleichzeitig ein Thread ist. Der Thread wird extra verwaltet. Sprite demoSprites[] = new Sprite[numSprites]; // 11 Bildchen im array //****************************************** // Ende der Vorrede und nun ersteinmal init //****************************************** public void init() { Image meinBild = null; // eine Variable vom Typ Image showStatus("lade Bilder..."); // showStatus ("...... zeigt // eine Nachricht am unteren // Fensterrand Das erste Sprite-Bild soll nun geladern werden.Es gibt mehrere Möglichkeiten, Bilder von Festplatte zu laden.Die Methode g e t I m a g e ( S t r i n g D a t e i n a m e ) tut es eigentlich schon. Stehen aber die Bilder z.B. in einem Verzeichnis auf einem Webserver und wird das Applet auf den heimischen Computer geladen, dann würde die Javamaschine auf dem häuslichen Computer nach den Dateien fahnden und sie dort nicht finden. Um dieses Problem zu beseitigen, wird mit der Methode g e t D o c u m e n t B a s e ( ) die URL des aktuellen Verzeichnisses geliefert, in dem das Applet liegt, dass den getImage-Aufruf enthält. meinBild = getImage(getDocumentBase(), "eki.gif"); // Bild geladen Das erste Bild ist drin und soll im Array die Position 0 einnehmen. Die Klasse Sprite enthält einen Konstruktor, der mehrere Parameter erwartet: Sprite (Image meinBild, int posX, int posY) Image Links-Oben x Links-Oben y Nun kann das Bild in den Array aus Sprites eingefügt werden: demoSprites[0] = new Sprite(meinBild, 100, getSize().height - 100); Seite 13 Eine Animation Virtuelle Lehrerfortbildung im Fach Informatik in Niedersachsen © Hans-Georg Beckmann 2004 Nun ist das Bild im Array und für die Darstellung am Bildschirm hat es die Koordinaten (100, Fensterhöhe - 100). Nun werden sofort die weiteren Attribute für diesen neuen Sprite gesetzt, so wie sie in der Klassendefinition vorgesehen sind. Das sind im Besonderen die Geschwindigkeit, die das Bildchen in x- und y-Richtung haben soll. Dann wird auch sofort die Methode s t a r t M o v e m e n t aufgerufen, die für die Bewegung zuständig sein soll. demoSprites[0].vx=5; demoSprites[0].vy=6; demoSprites[0].startMovement(); // Geschwindigkeitskomonente in x-Richtung // und in y-Richtung // flieg mal los Jetzt werden die andern Arraybilder geladen. Arrayeintrag Nummer 0 ist schon vergeben, also geht es mit 1 los. for (int i = 1; i < numSprites; i++) { meinBild = getImage(getDocumentBase(), "image" + i + ".gif"); demoSprites[i] = new Sprite(meinBild, 30+30*i, 30); // alle nebeneinander demoSprites[i].vx=(int)i/2 + 2; // unterschiedliche demoSprites[i].vy=(int)((10-i)/2); // Geschwindigkeiten if(demoSprites[i].vy == 0){demoSprites[i].vy=4;} // aber nicht Null demoSprites[i].startMovement(); }// Ende von for ..... showStatus(""); }// Ende von init // wenn alle drin dann Mitteilung weg //************************************************************************ // Nun kommt die Methode run, die geschrieben werden muss da wir runable // implementiert haben; run ist hier ans ganze Applet // gebunden und nicht an eine bestimmte Klasse //************************************************************************ public void run() { while (true) // wiederhole auf ewig { for (int i = 0; i < numSprites; i++) // wiederhole für alle 11 Sprites .. { demoSprites[i].tick(); // führe die Methode ticks aus ( siehe unten ) //*********************************************************** // Abprallen am Randes des Grafikbildschirms //********************************************************** if ((demoSprites[i].positionX<=15) || (demoSprites[i].positionX >= getSize().width-15)) demoSprites[i].vx=-demoSprites[i].vx; if ((demoSprites[i].positionY <= 15) || (demoSprites[i].positionY >= getSize().height-15)) demoSprites[i].vy=-demoSprites[i].vy; }// Ende von for ........ Seite 14 // wenn Rand // umkehren // umkehren Eine Animation Virtuelle Lehrerfortbildung im Fach Informatik in Niedersachsen © Hans-Georg Beckmann 2004 //************************************************************ // Try Anweisung ..... catch Ausnahmetyp x // hier also: bei Tread.sleep könnte ein Fehler entstehen // wenn der Ausnahmetyp InterruptException falsch ist // wenn das der Fall ist wird bei break weitergemacht //*********************************************************** update(getGraphics()); // die Methode update kommt noch weiter unten try { Thread.sleep(pause); // funktioniert auch ohne direkte Angabe der // Threadbezeichnung } catch (InterruptedException e) {break;} } }// Ende von run //************************************* // Standardmethoden für Treads hier // im Applet selbst überschrieben //************************************* public void start() { d emo = new Thread(this); // neuer Thread, this bezieht sich aufs // Applet demo.start(); // starte den Thread }// Ende von Start public void stop() { demo.stop(); demo = null; } // Ende von stop // demo auf null zeigen lassen Wenn eine Methode als final deklariert wird, dann kann sie während des Ablaufs des Programms nicht mehr überschrieben werden. Das wollten wir ja ohnehin nicht. Wieder wird der Code durch diese Maßnahme ein klein wenig schneller. Es wird wieder mal um die flimmerfreie Grafik mit Doublebuffering gehen. Also in ein OffScreenImage hineinmalen und dann erst das fertige Gesamtbild anzeigen lassen. Hier mal in der update-Routine untergebracht. public final void update(Graphics g) { Dimension dim = getSize(); // Ausmaße des Bildes if((offScreenImage==null) ||(dim.width != offScreenSize.width) || (dim.height != offScreenSize.height)) Was für eine Fehlerabfrage ! Wenn Das OffScreenImage auf null zeigt, also leer ist, oder die Bildgröße von der Größe des Offscreenbildes abweicht, dann wird ersteinmal ein passendes Bild mit c r e a t e I m a g e erzeugt, bei dem dann alles stimmt. { offScreenImage = createImage(dim.width, dim.height); offScreenSize = dim; Seite 15 Eine Animation Virtuelle Lehrerfortbildung im Fach Informatik in Niedersachsen © Hans-Georg Beckmann 2004 offScreenGraphics = offScreenImage.getGraphics();//hole die passende Grafik if (showTraces) //wollen wir eine Spur sehen ? offScreenGraphics.clearRect(0, 0,offScreenSize.width,offScreenSize.height); } // Ende der if((offScreenImage==null)... Abfrage if (!showTraces) // das wird es sein offScreenGraphics.clearRect(0, 0,offScreenSize.width,offScreenSize.height); paint(offScreenGraphics); // Methode paint wird mit offSCreenGraphics aufgerufen g.drawImage(offScreenImage,0,0,null); // Komplettes Bild aus dem OffScreen holen } Hier wird also unterschieden. Ist s h o w Tr a c e s wahr, dann will man Spuren sehen und auf dem Bild wird nichts gelöscht. Ist s h o w Tr a c e s unwahr, dann muss vor dem Neumalen der Bildschirm gelöscht werden, was mit c l e a r R e c t passiert. Wie schon in unseren bisherigen update-Methoden wird hier dann paint aufgerufen. Normalerweise haben wir dann in paint den g . d r a w I m a g e -Aufruf gepackt. Hier machen wir das innerhalb der update-Methode. Da aber vorher paint aufgerufen wird müssen wir mal sehen, was da passiert. public void paint(Graphics g) { for (int i = 0; i < numSprites; i++) { demoSprites[i].internPaint(g); // Achtung paint-Methode innerhalb von Sprite } // siehe unten } } // Ende von paint //****************************************************************** // Nun aber endlich die Klasse Sprite, die die Klasse // Panel erweitert. Panel ist ein Container, der die // Objekte enthält. Es könnte auch heissen : extends Component // das würde genauso funktionieren //****************************************************************** class Sprite extends Panel { //Konstanten, die ausserhalb der Klasse //benutzt aber nicht verändert werden koennen final static int MAXSPEED = 12; // Höchstgeschwindigkeit // Konstanten, die nur fuer die abgeleiteten Klassen zugaenglich sind // hier jeweils mit Starwerten besetzt protected final static int StandardGroesse = 30;// Grösse der Bildchen // ein // mit float float float float int paar geometrische Informationen die ebenfalls Startwerten initialisiert sind positionX = StandardGroesse; //Koordianten hier vom Typ float positionY = StandardGroesse; width = 0; height = 0; vx=5,vy=5; // Geschwindigkeitskomponenten //alles andere Seite 16 Eine Animation Virtuelle Lehrerfortbildung im Fach Informatik in Niedersachsen © Hans-Georg Beckmann 2004 protected boolean isMoving = false; protected Image myImage = null; // erstmal noch keine Bewegung // noch kein Bild da // Nun der Konstruktor, denm wir benutzen -- siehe oben Sprite (Image meinBild, int posX, int posY) { myImage = meinBild; positionX = posX; positionY = posY; setDimensions(); } Hier kommt nun die Methode, die später für die Bewegung des Sprites zuständig sein wird. Sie tut nichts anderes als ein Kontrollflag, das mit false besetzt war auf true zu stellen. So ein Aufwand für sowenig Programm ! public void startMovement () { isMoving = true; } // und nun das Gegenteil public void stopMovement () {isMoving = false;} //************************************************************** // Die Tick-Methode wird von der bei jedem // Tick aufgerufen. Sie bewegt das Sprite entlang seiner // Bewegungsrichtung, falls noetig. //************************************************************** public void tick () { if ((isMoving) && (vy != 0)&&(vy!=0)) // wenn in Bewegung dann .... { positionX =positionX+vx; // kennen wir schon positionY = positionY+vy; // hatten wir schon } } Jetzt kommt die interne paint-Methode, die nicht etwa die externe paint-Methode des Applets überschreibt. Diese Methode malt in das OffScreenImage hinein, so wie wir es schon mehrfach in den vorangegangenen Beispielen gesehen haben. //** malt das Sprite * public void internPaint(Graphics g) { g.drawImage(myImage, (int)(positionX), (int)(positionY), this); } Seite 17 Eine Animation Virtuelle Lehrerfortbildung im Fach Informatik in Niedersachsen © Hans-Georg Beckmann 2004 // die folgende Methode stellt die Bildmasse fest und gibt sie bekannt protected void setDimensions() { while (myImage.getHeight(this) == -1) // warte, bis das Bild geladen ist {} // tue nix width = myImage.getWidth(this); height = myImage.getHeight(this); } } Nun sind hoffentlich die internen Abläufe in diesem Programm hinreichend erklärt und es ist klar, dass sich hier nette Aufgaben anschließen sollten. Aufgabe: Erzeuge weitere Bilder, so dass sich 21 Sprites bewegen. Sorge dafür, das die Sprites elastische Stösse ausführen. Sorge dafür, dass ein grosses Sprite die kleinen bei Begegnung "auffressen" kann, dass also die kleinen Sprites dann aus dem Array und damit aus dem Thread entfernt werden. Aufgabe: Nun wird es etwas umfangreicher. Das klassische Spiel "Pong" soll programmiert werden. Dazu brauchen wir ein nettes Hintergrundbild und zwei kleine Bilder im Vordergrund - einen Ball und einen sogenannten Paddel. Der Ball soll - wie es nicht anders zu erwarten war - über den Bildschirm flitzen ( wie gehabt ). Am unteren Bildschirmrand aber bewegt sich der Paddel horizonatl hin - und her. Er wird mit den Cursortasten gesteuert und soll den Ball daran hindern, nach unten zu verschwinden. Eigentlich können wir alles, was für dieses Programm notwendig ist. Aber einige kleine Hilfen sollten es schon noch sein, damit man nicht an unnnötigen Stellen Probleme bekommt. Im folgenden Listing sind die wichtigsten Methoden und dargestellt, die das Spiel PONG benötigt. Die hier vorgestellte grobe Lösung arbeitet noch nicht mit Threads. Das Spiel mit Threads zum Laufen zu bringen bleibt am Ende als Aufgabe übrig. /* Es geht um den Spieleklassiker PONG. Ein Paddel unten und ein Ball der Boing macht und ein Hintergrundbild vor dem sich teilweise transparente GIF-Bilder bewegen H.-G. Beckmann aus 4/2004 */ import import import import java.awt.*; java.awt.event.*; java.applet.*; java.lang.*; // das Übliche zu Beginn Seite 18 Das Spiel Pong - ein Programmgerüst Virtuelle Lehrerfortbildung im Fach Informatik in Niedersachsen © Hans-Georg Beckmann 2004 public class Pong extends Applet { Image offScreenImage = null; Graphics offScreenGraphics = null; Dimension offScreenSize = null; int SpeedX=5,SpeedY=5; int SpeedPaddel=5; int paddelX = 300; int paddelY = 0; Image meinBallBild,meinPaddelBild; Image meinBG; int ballbreite,ballhoehe; int paddelbreite,paddelhoehe; int ballX=50,ballY=53; MediaTracker myTracker; kbListener myKBL= new kbListener(); boolean boolean boolean ....... ....... leftdown=false; rightdown=false; imSpiel=true; // // // // // // // ein Bild, dass wir nicht sehen dazu ein Graphicskontext Ausmaße des Bildes Ballgeschwindigkeit mit 5 initialisiert Paddel mit Tempo 5 initialisiert x-Position des Paddels y-Position des Paddels // // // // // // // // // // // die Bildchen das Hintergrundbild Dimensionen des Ballbildes Dimensionen des Paddelbildes Ballposition der Medienspürhund,siehe unten für die Keyboardevents eine eigene Klasse Taste <-- ist gedrückt Taste --> ist gedrückt ist der Ball noch im Spiel ? Hier sind wieder alle Variablen versammelt, die uns auch schon in den vorangegangenen Programmen begegnet sind. Neu ist der M e d i a Tr a c k e r . Dieser Medienspürhund hilft beim fehlerfreien Laden von Bildern. Das Einlesen von Bildern ( unterschiedlicher Größe) erfolgt in Java asynchron. Das heißt, es wird schon mit der nächsten Anweisung fortgefahren auch wenn der Ladevorgang noch nicht beendet ist. Das kann zur Darstellung von halben Bildern führen und wer möchte das schon. Neu ist auch der KeyboardListener - k b L i s t e n e r - der ähnlich finktioniert, wie der schon bekannte ActionListener für Buttons. Der kbListener ist für Tastaturevents zuständig und wird in einer eigenen Klassendefinition zu sehen sein. Zuerst aber zum MediaTracker: In der Methode init wird der Mediatracker wie folgt eingebaut: public void init() { // Hole Screendimension, erzeuge ein OffScreenBild, erzeuge dafür auch // einen Grafikkontext und erzeuge einen neuen Mediatracker offScreenSize = getSize(); offScreenImage = createImage(offScreenSize.width,offScreenSize.height); offScreenGraphics = offScreenImage.getGraphics(); MediaTracker myTracker = new MediaTracker(this); // ein neuer Spürhund showStatus("lade Bilder..."); // Nachricht am unteren Fensterrand Seite 19 Das Spiel Pong - ein Programmgerüst Virtuelle Lehrerfortbildung im Fach Informatik in Niedersachsen © Hans-Georg Beckmann 2004 //************************************************************************* // Lade die Bilder //************************************************************************* meinBG = getImage(getDocumentBase(), "BG.jpg"); Nun kommt der Mediatracker zum Einsatz addImage übergibt dem MediaTracker das angegebene Bild. Es bekommt im MediaTracker die angegebene ID-Nummer - hier die Nummer 0 myTracker.addImage(meinBG,0); try {myTracker.waitForID(0); } catch(InterruptedException e) {} // Achtung Hund, hier das erste Bild // warte bis Bild mit ID =0 fertig ist // Fehler abfangen für Bild ID 0 offScreenGraphics.drawImage(meinBG,0,0, null); // jetzt malen Damit ist das Hintergrundbild eingelesen und im OffScreen gemalt. Nun geht das mit den beiden andern Bildern genauso: meinBallBild = getImage(getDocumentBase(), "Ball2.gif"); myTracker.addImage(meinBallBild,1); // Achtung Hund, hier das nächste Bild try {myTracker.waitForID(1); // warte auf Bild mit ID = 1 } catch(InterruptedException e) {} offScreenGraphics.drawImage(meinBallBild,ballX,ballY, null); ballbreite=meinBallBild.getWidth(this); // Bildbreite falls man sie braucht ballhoehe=meinBallBild.getHeight(this); // Bildhöhe falls man sie braucht meinPaddelBild= getImage(getDocumentBase(), paddelX=300; // paddelY=370; // myTracker.addImage(meinPaddelBild,2); // try {myTracker.waitForID(2); // } catch(InterruptedException e) {} "Paddel.gif"); an dieser Position soll das Bild erscheinen und der Hund passt wieder auf auch Bild 3 sicher laden lassen offScreenGraphics.drawImage(meinPaddelBild,paddelX, paddelY, null); paddelbreite=meinPaddelBild.getWidth(this); // Paddelausmaße, falls man sie paddelhoehe=meinPaddelBild.getHeight(this); // später braucht showStatus(""); // alles erledigt //**************************************************** // Nun wird dem ganzen Applet ein KeyListener // zugeordnet, desssen Klassendefiniton unten steht //**************************************************** addKeyListener(myKBL); } // Ende von init Seite 20 Das Spiel Pong - ein Programmgerüst Virtuelle Lehrerfortbildung im Fach Informatik in Niedersachsen © Hans-Georg Beckmann 2004 Jetzt die paint-Methode, in der sich in dieser Fassung alles abspielt //********************************************************** public void paint(Graphics g) { if (imSpiel==true) { ballX=ballX+SpeedX; // Ball in x-Richtung //************ Nun prüfen , ob am Rande reflektiert wird ******************* if((ballX >offScreenSize.width-25)||(ballX <10)) {SpeedX=-SpeedX;} ballY=ballY+SpeedY; Spannend nun die Abfrage in Y-Richtung, wenn der Ball in Höhe des Paddels kommt. Es muss geprüft werden, ob der Paddel passend steht, sonst fliegt der Ball vorbei und ist aus dem Spiel. if(ballY >= paddelY-22) // wir kommen in Paddelhöhe { if((ballX>=paddelX-10)&&(ballX<paddelX+100)) // Ball im Paddelbereich ? { SpeedY=-SpeedY; // Am Paddel -->dann reflektieren imSpiel = true; } else {imSpiel =false;} // am Paddel vorbeigeflogen } // **************** Reflexion am oberen Rand hier einfach ******************* if (ballY<10) {SpeedY=-SpeedY;} //********************************************************** // die Tasten abfragen mit Anschlag am rechten oder // linken Spielfeldrand //********************************************************** if(leftdown) // ist die Linkspfeiltaste gedrückt ? { paddelX=paddelX-SpeedPaddel;if (paddelX<=5) {paddelX=5;} } if(rightdown) // ist die Rechtspfeiltaste gedrückt ? { paddelX=paddelX+SpeedPaddel;if (paddelX>=520) {paddelX=520;} } //Im offScreenImage alles auf den neuesten Stand bringen und dann an // g übergeben offScreenGraphics.drawImage(meinBG,0, 0, null); // jetzt malen offScreenGraphics.drawImage(meinBallBild,ballX, ballY, null); // jetzt malen offScreenGraphics.drawImage(meinPaddelBild,paddelX, paddelY, null); g.drawImage(offScreenImage,0,0,this); // zeichnet das OffScreenImage jetzt in g repaint(10); } // Ende von imSpiel = true. Ball unten druch, dann hält das Spiel einfach an } Seite 21 Das Spiel Pong - ein Programmgerüst Virtuelle Lehrerfortbildung im Fach Informatik in Niedersachsen © Hans-Georg Beckmann 2004 Diese Art, die Bilder immer auf dem animierten aktuellen Stand zu zeigen hat offensichtlich Nachteile. Trotz Malens im OffScreen treten Wackeleffekte auf. Das hat damit zu tun, dass das System alle drei Bilder komplett neu malen muss. Da das Hintergrundbild den ganzen Bildschirm einnimmt, kostet es auch viel Zeit, das Bild neu zu malen. Hier gibt es eine gute Lösung über das sogenannte Clipping. Das ist die Einschränkung des neu zu malenden Bereichs auf einen rechteckigen Bereich, der sehr viel kleiner ist als das ganze Bild. In unserem Spiel muss eine kleiner Bereich des Hintergrundbereichs neu gemalt werden, wenn der Ball an der alten Position verschwinden soll und dort das Hintergrundbild wieder erscheinen soll. Das gilt auch für den Paddel, dessen Position sich verändert. Auch an desses alter Position muss das Hintergrundbild wieder neu gemalt werden. Zuerst wird mit der Methode setClip(int x, int y, int breit, int hoch) festgelegt an welcher Stelle ein Bereich mit welchen Ausmaßen neu zu zeichnen ist. Im nachfolgenden drawImage - Befehl wird dann nur dieser Bereich bearbeitet. Man bemerkt sofort, dass die Animation deutlich geschmeidiger abläuft. Damit sollten die obigen Programmzeilen ersetzt werden: offScreenGraphics.setClip(ballX-10,ballY-10,50,50); //wo war der Ball ? offScreenGraphics.drawImage(meinBG,0, 0, null); // jetzt malen in Clippingregion if(imSpiel==true) { // nun neuen Ball, falls der noch im Spiel ist offScreenGraphics.drawImage(meinBallBild,ballX, ballY, null); } // gleiche Übung für den Paddel offScreenGraphics.setClip(paddelX-10,paddelY,paddelbreite+20,paddelhoehe); offScreenGraphics.drawImage(meinBG,0, 0, null); // jetzt malen offScreenGraphics.drawImage(meinPaddelBild,paddelX, paddelY, null);// neuer Paddel // Jetzt das ganze Bild in den Vordergrund g.drawImage(offScreenImage,0,0,this); // zeichnet das OffScreenImage jetzt in g repaint(10); } // Ende von imSpiel = true .Ball unten durch, dann hält das Spiel einfach an } public void update(Graphics g) { paint(g); } Die Werte in der setClip-Methode sind hier teilweise "per Hand" gesetzt. Mann kann sie aber komplett aus den Bildausmaßen ermitteln. Beachten Sie aber, dass hier kleine gemeine Fehler auftreten können, da sich der Ball mal nach links bewegt ( seine x-Koordinaten werden dann kleiner ) oder aber nach rechts ( seine x-Koordinaten werden dann größer ). Bleibt zum guten Schluss noch der kbListener, der die Tastaturabfrage für uns erledigt. Darin wird mit vorgegebenen Konstanten gearbeitet, nämlich VK_LEFT für die Linkscursortaste, VK_RIGHT für die Rechtscursortaste. Einige der pflichtweise zu überschreibenden Methoden sind hier leer gelassen worden. Seite 22 Das Spiel Pong - ein Programmgerüst Virtuelle Lehrerfortbildung im Fach Informatik in Niedersachsen © Hans-Georg Beckmann 2004 //*************************************************************************** // Hier nun die Klasse für den KeyListener // es muss mindestens keyPressed, keyTyped und keyReleased geschrieben werden // dabei können diese Methoden auch leer sein //**************************************************************************** class kbListener implements KeyListener { public void keyPressed (KeyEvent myEvent) { if (myEvent.getKeyCode()==KeyEvent.VK_LEFT) {leftdown=true;} if (myEvent.getKeyCode()==KeyEvent.VK_RIGHT) {rightdown=true;} // VK_LEFT ist vorgegeben // VK_RIGHT ist vorgegeben } public void keyTyped (KeyEvent myEvent) {} public void keyReleased (KeyEvent myEvent) // leer // man muss ja auch merken, wenn die // Taste nicht mehr gedrückt ist { if(leftdown){leftdown=false;} if(rightdown){rightdown=false;} } } // Ende von KeyListener Klasse } // Ende vom Applet Man muss die Keyboardabfrage nicht in einer eigenen Klasse behandeln. Wie auch schon beim Abfragen von ActionEvents ist es möglich, dem ganzen Applet mit public class Pong extends Applet implements KeyListener einen KeyListener zu spendieren, der dann in der Methode init mit addKeyListener(this); dem Applet zugewiesen wird. Dann muss man nur noch dieoben angegebenen Methoden überschreiben. Wenn es wieder Laufzeitprobleme geben sollte, dann verzichten sie auf das OffScreenImage und malen alles direkt in der paint-Methode in den Grafikkontext g hinein. Dies ist nun insgesamt ein ausbaufähiges Programmgerüst, das auf mancherlei Weise erweitert oder verbessert werden soll. Seite 23 Das Spiel Pong - Aufgaben Virtuelle Lehrerfortbildung im Fach Informatik in Niedersachsen © Hans-Georg Beckmann 2004 Aufgaben Bringen sie das Programm mit Threads zum Laufen. Fügen sie ein Punktezähler hinzu, der am Spielfeldrand Punkte mitzählt. Fügen sie ein wenig Zufall bei der Ballreflexion am Paddel ein. (Geschwindigkeits - und Richtungsänderungen ). Geben sie eine Maximalzahl von Bällen vor. Wenn alle verbraucht sind, ist das Spiel zu Ende. Bauen sie Hindernisse auf, die mit dem Ball "abgeschossen" werden können . Seite 24