MarktundTechnik Home-Page Previous Page TOC Index Next Page See Page



11. Tag:

Mehr Animationen, Bilder und Sound

von Laura Lemay

Animationen machen Spaß und sind einfach in Java, jedoch sind den integrierten Java-Methoden für Linien, Fonts und Farben gestalterisch Grenzen gesetzt. Für wirklich interessante Animationen müssen Sie für jeden Rahmen der Animation selbst Bilder bereitstellen und mit Sound ausmalen. Heute lernen Sie das Entwikkeln anspruchsvollerer Animationen. Sie werden Bilder und Sound in Java-Applets integrieren.

Sie lernen in der heutigen Lektion insbesondere:

Abrufen und Verwenden von Bildern

Die Handhabung von Bildern ist in Java einfach. Die Image-Klasse in java.awt bietet abstrakte Methoden zum Darstellen von üblichen Eigenschaften für Bilder. Insbesondere die Methoden, die in Applet und Graphics definiert sind, bieten alles, um Bilder in einem Applet zu laden und anzuzeigen. In diesem Abschnitt lernen Sie, wie Bilder geholt und in Java-Applets angezeigt werden.

Bilder holen

Um ein Bild in Ihrem Applet anzeigen zu können, müssen Sie das Bild zuerst über das Netz in Ihr Java-Programm laden. Bilder sind von den Java-Klassendateien getrennt in eigenen Dateien gespeichert, deshalb müssen Sie Java mitteilen, wo danach gesucht werden soll.

Die Applet-Klasse umfaßt eine Methode namens getImage, mit der ein Bild geladen und automatisch eine Instanz der Image-Klasse erstellt wird. Um sie zu verwenden, brauchen Sie nur die java.awt.Image-Klasse zu importieren und getImage mit der Adresse (URL) des gewünschten Bildes zu versehen. Für diesen Schritt gibt es zwei Vorgehensweisen:

Die erste Vorgehensweise ist zwar einfacher (man fügt einfach den URL als url-Objekt ein), jedoch ist die zweite flexibler. Da Sie Java-Dateien kompilieren, müssen Sie bedenken, daß Sie im Fall der Einbindung eines hartcodierten URLs alle Java-Dateien erneut kompilieren müssen, wenn Sie die Dateien an eine andere Stelle verlagern.

Wir arbeiten deshalb mit der zweiten Vorgehensweise. Die Applet-Klasse bietet auch zwei Methoden, mit denen das URL-Argument in getImage eingebunden werden kann:

Ob Sie getDocumentBase() oder getCodeBase() verwenden, hängt davon ab, ob die Bilder mit Ihren HTML-Dateien oder Ihren Java-Klassendateien in Bezug stehen. Verwenden Sie die Methode, die sich für Ihre Situation besser eignet. Beachten Sie, daß beide Methoden flexibler sind als die Verwendung eines hartcodierten URLs oder Pfadnamens in der getImage-Methode. Mit getDocumentBase oder getCodeBase findet Java Ihre HTML-Dateien und Applets auf jeden Fall, auch wenn Sie sie verschieben.

Nachfolgend ein paar Beispiele mit getImage, um Ihnen eine Vorstellung über den Gebrauch dieser Methode zu geben. Hier holt der erste Aufruf von getImage die Datei im spezifizierten URL (»http://www.server.com/files/image.gif«). Ändert sich ein Teil dieses URL, müssen Sie Ihr Java-Applet erneut kompilieren, um den neuen Pfad zu berücksichtigen:

Image img = getImage(

   new url("http://www.server.com/files/image.gif"));

In der folgenden Form von getImage befindet sich die image.gif-Datei im gleichen Verzeichnis wie die HTML-Dateien, auf die dieses Applet Bezug nimmt:

Image img = getImage(getdocumentBase(), "image.gif");

In der folgenden ähnlichen Form ist die Datei image.gif im gleichen Verzeichnis wie das Applet:

Image img = getImage(getCodeBase(), "image.gif");

Werden zahlreiche Bilddateien verwendet, legt man sie üblicherweise in einem eigenen Unterverzeichnis ab. In der folgenden Form sucht getImage nach der Datei image.gif im Verzeichnis images, in dem sich auch das Java-Applet befindet:

Image img = getImage(getCodeBase(), "images/image.gif");

Kann Java die angegebene Datei nicht finden, gibt getImage Null aus. Das Programm läuft dann zwar weiter, jedoch wird das betreffende Bild nicht am Bildschirm angezeigt.

Die derzeitige Java-Version unterstützt nur Bilder im GIF- und JPEG-Format. Andere Bildformate sind eventuell in späteren Versionen verfügbar.

Bilder zeichnen

getImage dient nur zum Abrufen und Bereitstellen eines Bildes in einer Instanz der Image-Klasse. Nachdem dies geschehen ist, möchten Sie sicherlich mit dem Bild etwas anfangen. Zweifellos soll das Bild angezeigt werden. Die Graphics-Klasse bietet zwei Methoden für diesen Zweck, die beide drawImage heißen.

Die erste Version von drawImage hat vier Argumente: das anzuzeigende Bild, die x- und y-Position der linken oberen Ecke und folgendes:

void paint() {

   g.drawImage(img, 10, 10, this);

}

Diese Form führt das aus, was man logischerweise erwartet: Sie zeichnet das Bild in seiner Originalabmessung mit der oberen linken Ecke in der angegebenen x- und y-Position. Listing 11.1 enthält den Code für ein sehr einfaches Applet, das ein Bild namens ladybug.gif lädt und anzeigt. Den daraus resultierenden Marienkäfer sehen Sie in Abb. 11.1.

Listing 11.1: Das Ladybug-Applet

1:   import java.awt.Graphics;

2:   import java.awt.Image;

3:

4:   public class LadyBug extends java.applet.Applet {

5:

6:      Image bugimg;

7:

8:      public void init() {

9:         bugimg = getImage(getCodeBase(),

10:            "images/ladybug.gif");

11:      }

12:

13:      public void paint(Graphics g) {

14:         g.drawImage(bugimg,10,10,this);

15:      }

16:   }

Die zweite Form von drawImage hat sechs Argumente: Das zu zeichnende Bild, die x- und y-Koordinaten, die Breite und Höhe des Kastens, in dem das Bild angezeigt wird, und this. Sind die Argumente für die Breite und Höhe der Bildbox kleiner oder größer als das Bild selbst, wird das Bild automatisch skaliert, damit es in die Box paßt. Anhand dieser zusätzlichen Argumente können Sie Bilder in jede beliebige Rahmengröße einpassen (beachten Sie aber, daß sich die Qualität von Bildern durch übermäßiges Skalieren verschlechtern kann).

Beim Skalieren von Bildern empfiehlt es sich, zuerst die tatsächliche Größe des zu ladenden Bildes herauszufinden, damit es in einem spezifischen Prozentsatz skaliert werden kann, um Verzerrungen in beiden Richtungen zu vermeiden. Zwei für die Image-Klasse definierte Methoden ermöglichen Ihnen dies: getWidth() und getHeight(). Beide Methoden haben nur ein Argument, eine Instanz von ImageObserver, die verwendet wird, um das Laden eines Bildes zu verfolgen (mehr darüber später). Meist genügt this als Argument für getWidth() oder getHeight().

Abbildung 11.1: Das Ladybug-Bild

Haben Sie das Marienkäferbild beispielsweise in einer Variablen namens bugimg gespeichert, gibt diese Zeile die Breite des Bildes in Pixeln aus:

theWidth = bugimg.getWidth(this);

Listing 11.2 enthält einen anderen Code für das Marienkäferbild, in dem das Bild mehrmals auf verschiedene Größen skaliert wird (das Ergebnis ist in Abb. 11.2 ersichtlich).

Listing 11.2: Skalierte Marienkäfer

1:   import java.awt.Graphics;

2:   import java.awt.Image;

3:

4:   public class LadyBug2 extends java.applet.Applet {

5:

6:      Image bugimg;

7:

8:      public void init() {

9:         bugimg = getImage(getCodeBase(),

10:            "images/ladybug.gif");

11:      }

12:

13:      public void paint(Graphics g) {

14:         int iwidth = bugimg.getWidth(this);

15:         int iheight = bugimg.getHeight(this);

16:         int xpos = 10;

17:

18:         // 25%

19:      g.drawImage(bugimg,xpos,10,

20:         iwidth / 4, iheight / 4, this);

21:

22:         // 50%

23:         xpos += (iwidth / 4) + 10;

24:         g.drawImage(bugimg, xpos , 10,

25:            iwidth / 2, iheight / 2, this);

26:

27:         // 100%

28:         xpos += (iwidth / 2) + 10;

29:         g.drawImage(bugimg, xpos, 10, this);

30:

31:         // 150% x, 25% y

32:         g.drawImage(bugimg, 10, iheight + 30,

33:            (int) (iwidth * 1.5), iheight / 4, this);

34:      }

35:   }

Die ganze Zeit über habe ich das letzte Argument von drawImage nicht erwähnt: das mysteriöse this, das auch als Argument für getWidth() und getHeight() erscheint. Warum wird dieses Argument benutzt? Es dient zur Weitergabe eines Objekts, das als ImageObserver (d. h. ein Objekt, das die ImageObserver-Schnittstelle implementiert) benutzt wird. ImageObserver ermöglicht Ihnen, den Verlauf des Ladevorgangs eines Bildes mitzuverfolgen und Entscheidungen zu treffen, wann das Bild ganz oder teilweise geladen werden soll. Die Applet-Klasse, von der Ihr Applet erbt, enthält Standardeigenschaften zum Beobachten von Bildern - deshalb das this-Argument für drawImage(), getWidth() und getHeight(). Der einzige Grund, warum statt dessen ein anderes Argument benutzt wird, ist das Verfolgen zahlreicher Bilder, die synchron geladen werden. Weitere Einzelheiten finden Sie in der Klasse java.awt.image.ImageObserver.

Bilder ändern

Abgesehen von den obigen grundlegenden Methoden zur Handhabung von Bildern enthält das Paket java.awt.image weitere Klassen und Schnittstellen, mit denen Sie Bilder und deren interne Farben ändern oder Bitmap-Bilder manuell erstellen können. Die meisten dieser Klassen setzen gewisse Kenntnisse in der Bildverarbeitung voraus. Diese Arbeiten sind nicht Gegenstand dieses Einführungsbuches in Java. Falls Sie über diese Kenntnisse verfügen (oder interessiert sind, sich daran zu üben), werden Sie die in java.awt.image enthaltenen Klassen nützlich finden. Sehen Sie sich auch den Beispielcode zum Erstellen und Verwenden von Bildern an, der im Java-Development-Kit enthalten ist.

Entwickeln von Animationen mit Bildern

Das Erstellen von Animationen mit Bildern ist ähnlich wie das Erstellen von Bildern mit Fonts, Farben und Formen. Sie wenden die gleichen Methoden, die gleichen Prozeduren zum Zeichnen, Nachzeichnen und Reduzieren des Flimmereffekts an, die Sie gestern gelernt haben. Der einzige Unterschied liegt darin, daß Sie nicht mit Zeichenmethoden, sondern mit verschiedenen Bildern arbeiten.

Die wohl beste Möglichkeit, aufzuzeigen, wie Bilder für Animationen verwendet werden, ist das schrittweise Durcharbeiten eines praktischen Beispiels. Im folgenden befassen wir uns mit einer Animation eines Kätzchens namens Neko.

Ein Beispiel: Neko

Neko ist eine kleine Animation bzw. ein Spiel für den Macintosh, das von Kenji Gotoh 1989 geschrieben und gezeichnet wurde. »Neko« ist Japanisch für »Katze«. Die Animation zeigt ein kleines Kätzchen, das den Mauszeiger über den Bildschirm jagt, schläft, sich kratzt und allgemein posierlich anzusehen ist. Das Neko-Programm wurde inwischen auf jede mögliche Plattform portiert und als beliebter Screensaver umgeschrieben.

In diesem Beispiel implementieren Sie eine kleine Animation auf der Grundlage der Neko-Originalgrafiken. Da das Kätzchen im Neko-Originalprogramm autonom ist (es kann die Kanten des Fensters »ertasten«, sich umdrehen und in eine andere Richtung rennen), veranlaßt dieses Applet lediglich, daß Neko von links in den Bildschirm hereinspringt, in der Mitte stehenbleibt, gähnt, sich am Ohr kratzt, ein wenig schläft und dann rechts davonrennt.

Das ist das bei weitem umfangreichste der in diesem Buch diskutierten Applets. Wenn ich es hier voll aufführen und beschreiben würde, wären Sie Tage nur damit beschäftigt. Deshalb beschreibe ich einzelne Teile dieses Applets und lasse die Grundlagen weg, d. h. alles, was Sie gestern über das Starten und Stoppen von Threads, die run()-Methode usw. gelernt haben. Der restliche Code wird im weiteren Verlauf der heutigen Lektion aufgeführt, so daß Sie alles selbst zusammensetzen können.

Bevor wir mit dem Schreiben des Java-Codes für eine Animation beginnen, sollten Sie alle Bilder, aus denen sich die Animation zusammensetzt, erst einmal sehen. Diese Version von Neko umfaßt neun Bilder (das Original hat 36), die in Abb. 11.3 dargestellt sind.

Abbildung 11.2: Die Neko-Bilder

Ich habe diese Bilder in einem Unterverzeichnis des Applet-Verzeichnisses unter dem Namen images gespeichert. Wo Sie Ihre Bilder speichern, ist nicht so wichtig. Sie müssen sie nur wiederfinden, da sie für die Animation benötigt werden.

Nun beginnen wir mit dem Applet. Das Grundkonzept der Animation ist die Verwendung mehrerer Bilder, die Sie einzeln und schnell nacheinander anzeigen, so daß der Eindruck von Bewegung entsteht. Dies kann in Java am einfachsten dadurch realisiert werden, daß die Bilder in einem Array der Image-Klasse gespeichert werden, auf die dann mit einer speziellen Variablen jeweils Bezug genommen wird.

Die Klasse java.util enthält die Klasse HashTable, die eine Hash-Tabelle implementiert. Bei einer großen Anzahl von Bildern können Sie mit einer Hash-Tabelle alle Bilder schneller auffinden und abrufen als mit einem Array. Da wir hier mit einer relativ kleinen Zahl von Bildern arbeiten, benutze ich in diesem Beispiel ein Array.

Für das Neko-Applet füge ich Instanzvariablen ein, um beide Aspekte zu implementieren: Ein Array für die Bilder namens nekopics und eine Variable vom Typ Image für das aktuelle Bild:

Image nekopics[] = new Image[9];

Image currentimg;

Da Sie die Position des aktuellen Bildes zwischen den Methoden in diesem Applet weitergeben müssen, ist es notwendig, die aktuellen x- und y-Positionen zu verfolgen. y bleibt bei diesem Applet konstant, jedoch kann x variieren. Wir fügen für diese zwei Positionen folgende Instanzvariablen ein:

int xpos;

int ypos = 50;

Nun befassen wir uns mit dem Körper des Applets. Bei der Initialisierung des Applets werden alle Bilder eingelesen und im nekopics-Array gespeichert. Das ist eine Operation, die in einer init()-Methode besonders gut funktioniert.

Angesichts der Tatsache, daß wir neun Bilder mit neun verschiedenen Dateinamen haben, könnten wir jedes Bild mit einem separaten getImage aufrufen. Ein wenig Tipparbeit können Sie sich jedoch durch das Erstellen eines Arrays mit den Dateinamen (ein Zeichenketten-Array namens nekosrc) ersparen. Dann verwenden Sie eine for-Schleife, um alle Dateien zu durchlaufen. Die init()-Methode für das Neko-Applet, die alle Bilder des nekopics-Arrays lädt, sieht so aus:

public void init() }

   String nekosrc[] = { "right1.gif", "right2.gif",

      "stop.gif", "yawn.gif", "scratch1.gif",

      "scratch2.gif", "sleep1.gif", "sleep2.gif",

      "awake.gif" };

   for (int i=0; i < nekopics.length; i++) {

      nekopics[i] = getImage(getCodeBase(),

         "images/" + nekosrc[i]);

   }

}

Beachten Sie im Aufruf von getImage, daß das Verzeichnis, in dem die Bilder gespeichert sind, im Pfad enthalten ist.

Nach dem Laden der Bilder wird im nächsten Schritt die Animation der Applet-Teile gestartet. Dies realisieren Sie innerhalb der run()-Methode des Applet-Threads. Das Kätzchen Neko macht fünf Sachen:

Da Sie dieses Applet dadurch animieren können, daß Sie lediglich das richtige Bild zur richtigen Zeit am Bildschirm ausgeben, ist es sinnvoller, dieses Applet so zu schreiben, daß möglichst viele Neko-Aktivitäten in einzelnen Methoden enthalten sind. Auf diese Weise können Sie einige Aktivitäten (insbesondere, wenn Neko rennt) wiederverwenden, wenn Sie Neko in einer anderen Reihenfolge aktiv sein lassen wollen.

Wir beginnen mit dem Erstellen einer Methode, durch die Neko rennt. Da sie diese Methode zweimal verwenden, empfiehlt sich dafür eine generische Methode. Wir erstellen also die nekorun-Methode, die zwei Argumente erhält: Je eine x-Position zum Starten und Beenden. Neko rennt dann zwischen diesen zwei Positionen hin und her (y bleibt konstant).

Neko rennt in zwei Bildern. Um die Laufwirkung zu erzielen, müssen Sie zwischen diesen zwei Bildern (die in Position 0 und 1 der Bilderreihe gespeichert sind) abwechseln und sie über den Bildschirm bewegen. Der Bewegungsteil ist eine einfache for-Schleife zwischen dem start- und end-Argument, wobei die globale x-Position auf den aktuellen Schleifenwert gesetzt wird. Wechseln der Bilder bedeutet hier lediglich pausieren, um zu sehen, welches in einer Schleife aktiv ist, und dann das andere als aktuelles Bild zuweisen. Schließlich wird bei jedem neuen Rahmen repaint und sleep kurz aufgerufen.

Da das Kätzchen in dieser Animation eigentlich in unterschiedlichen Abständen viel schläft, ist es sinnvoll, eine Methode mit den Schlafszenen für die entsprechenden Zeiten einzurichten. Wir nennen diese Methode pause - hier die Definition:

void pause(int time) {

   try { Thread.sleep(time); }

   catch (InterruptedException e) { }

}

Wir wenden uns nun wieder der nekorun-Methode zu. nekorun wird ab der start-Position bis zur end-Position wiederholt. Bei jedem Schleifenwechsel wird ihre aktuelle x-Position gesetzt. Ferner wird currentimg rechts vom Animationsrahmen gesetzt, dann wird repaint aufgerufen und pausiert. Die Definition von nekorun ist wie folgt:

void nekorun(int start, int end) {

   for (int i = start; i < end; i+=10) {

      this.xpos = i;

      // Bilder wechseln

      if (currentimg == nekopics[0])

         currentimg = nekopics[1];

      else if (currentimg == nekopics[1])

         currentimg = nekopics[0];

      repaint();

      pause(150);

   }

}

Beachten Sie, daß die Schleife in der zweiten Zeile um zehn Pixel erhöht wird. Warum zehn und nicht fünf oder acht? Der richtige Wert wird hierfür durch Experimentieren, was am besten aussieht, ermittelt. Zehn schien mir für diese Animation das beste Ergebnis zu liefern. Wenn Sie eine Animation schreiben, müssen Sie mit den beiden Entfernungen und den Schlafzeiten spielen, bis Sie die richtigen Bewegungen erreichen.

Da wir eben von repaint gesprochen haben, wollen wir hier die paint()-Methode untersuchen, die jeden Rahmen zeichnet. Hier ist die paint-Methode sehr einfach. paint ist nur für die Ausgabe des aktuellen Bildes an den aktuellen x- und y-Positionen zuständig. Diese Informationen sind in globalen Variablen gespeichert, so daß die paint-Methode nur eine Zeile umfaßt:

public void paint(Graphics g) {

   g.drawImage(currentimg, xpos, ypos, this);

}

Jetzt kehren wir wieder zur run()-Methode zurück, wo die Hauptverarbeitung dieser Animation stattfindet. Sie haben die nekorun-Methode erstellt. In run rufen Sie diese Methode mit den entsprechenden Werten auf, damit Neko vom rechten Bildschirmrand in die Mitte rennt:

// Kätzchen rennt vom Bildschirmrand zur Mitte

nekorun(0, this.size().width / 2);

Danach bleibt Neko stehen und gähnt. Sie haben ein Bild für diese beiden Aktionen (Position 2 und 3 im Array), deshalb brauchen Sie keine getrennten Methoden dafür. Sie setzen lediglich das entsprechende Bild, rufen repaint() auf und pausieren entsprechend lang. In diesem Beispiel wird jedesmal beim Stoppen und Gähnen eine Sekunde pausiert. Hier ist der Code:

// Kätzchen bleibt stehen 

currentimg = nekopics[2];

repaint();

pause(1000);

// Kätzchen gähnt

currentimg = nekopics[3];

repaint();

pause(1000);

Dann kratzt sich das Kätzchen. Für diesen Teil der Animation gibt es keine Horizontale. Sie wechseln zwischen den zwei Kratzbildern (Position 4 und 5 des Arrays). Hierfür definieren wir aber eine separate Methode.

Die nekoscratch-Methode hat ein Argument für die Kratzhäufigkeit. Mit diesem Argument können Sie die Aktion wiederholen, dann in der Schleife zwischen den zwei Kratzbildern wechseln und repaint aufrufen:

void nekoscratch(int numtimes) {

   for (int i = numtimes; i > 0; i --) {

      currentimg = nekopics[4];

      repaint();

      pause(150);

      currentimg = nekopics[5];

      repaint();

      pause(150);

   }

}

Mit dem Argument 4 rufen Sie dann nekoscratch in der run-Methode auf:

// Kätzchen kratzt sich viermal

nekoscratch(4);

Und weiter geht's! Nach dem Kratzen gibt es zwei Bilder, auf denen Neko schläft (Position 6 und 7 im Array). Diese Bilder wechseln mehrmals ab. Hier verwenden wir die nekosleep-Methode mit einem Zahlenargument und animieren es im Wechsel:

void nekosleep(int numtimes) {

   for (int i = numtimes; i > 0; i --) {

      currentimg = nekopics[6];

      repaint();

      pause(250);

      currentimg = nekopics[7];

      repaint();

      pause(250);

   }

}

nekosleep wird in der run()-Methode wie folgt aufgerufen:

// Kätzchen schläft fünfmal im Wechsel

nekosleep(5);

Wir kommen zum Ende des Applets. Neko wacht auf und rennt über den rechten Bildschirmrand davon. Das Aufwachen ist das letzte Bild des Arrays (Position 8). Sie können zum Fertigstellen die nekorun-Methode wiederverwenden:

// Kätzchen wacht auf und rennt davon

currentimg = nekopics[8];

repaint();

pause(500);

nekorun(xpos, this.size().width + 10);

Eines fehlt noch, um das Applet fertigzustellen. Alle Bilder der Animation haben einen weißen Hintergrund. Das Zeichnen dieser Bilder auf dem Applet-Standardhintergrund (Mittelgrau) ergibt einen kaum sichtbaren weißen Kasten um jedes Bild. Dies wollen wir vermeiden, indem wir den Applet-Hintergrund am Anfang der run()-Methode auf Weiß setzen:

setBackground(Color.white);

Der Code für dieses Applet ist ziemlich umfangreich und für eine eher einfache Animation sind viele Methoden zu schreiben, jedoch ist das alles nicht kompliziert. Der Kern jeder Java-Animation ist die Einrichtung des Rahmens, dann wird repaint() aufgerufen, damit der Bildschirm gezeichnet wird.

Beachten Sie, daß in diesem Applet nichts unternommen wurde, um das Flimmern zu beseitigen. Die hier verwendeten Bilder und der Zeichnungsbereich sind klein, so daß bei diesem Applet der Flimmereffekt nicht ins Gewicht fällt. Bei jeder Java-Animation empfiehlt sich, mit den einfachen Dingen zu beginnen und dann die verschiedenen Eigenschaften hinzuzufügen.

Am Schluß dieses Abschnitts erhalten Sie in Listing 11.3 den kompletten Code für das Neko-Applet.

Listing 11.3: Das komplette Neko-Applet

36:   import java.awt.Graphics;

37:   import java.awt.Image;

38:   import java.awt.Color;

39:

40:   public class Neko extends java.applet.Applet

41:      implements Runnable {

42:

43:      Image nekopics[] = new Image[9];

44:      Image currentimg;

45:      Thread runner;

46:      int xpos;

47:      int ypos = 50;

48:

49:   public void init() {

50:         String nekosrc[] = { "right1.gif", "right2.gif",

51:         "stop.gif", "yawn.gif", "scratch1.gif",

52:         "scratch2.gif", "sleep1.gif", "sleep2.gif",

53:         "awake.gif" };

54:

55:      for (int i=0; i < nekopics.length; i++) {

56:         nekopics[i] = getImage(getCodeBase(),

57:         "images/" + nekosrc[i]);

58:      }

59:

60:   public void start() {

61:      if (runner == null) {

62:         runner = new Thread(this);

63:         runner.start();

64:      }

65:   }

66:

67:   public void stop() {

68:      if (runner != null) {

69:         runner.stop();

70:         runner = null;

71:      }

72:   }

73:

74:   public void run() {

75:

76:      setBackground(Color.white);

77:

78:      // Kätzchen rennt vom Bildschirmrand zur Mitte

79:      nekorun(0, this.size().width / 2);

80:

81:      // Kätzchen bleibt stehen

82:      currentimg = nekopics[2];

83:      repaint();

84:      pause(1000);

85:

86:      // Kätzchen gähnt

87:      currentimg = nekopics[3];

88:      repaint();

89:      pause(1000);

90:

91:      // Kätzchen kratzt sich viermal

92:      nekoscratch(4);

93:

94:      // Kätzchen schläft im Wechsel fünfmal

95:      nekosleep(5);

96:

97:      // Kätzchen wacht auf und rennt davon

98:      currentimg = nekopics[8];

99:      repaint();

100:      pause(500);

101:      nekorun(xpos, this.size().width + 10);

102:      }

103:

104:      void nekorun(int start, int end) {

105:         for (int i = start; i < end; i+=10) {

106:            this.xpos = i;

107:            // Bildwechsel

108:            if (currentimg == nekopics[0])

109:         currentimg = nekopics[1];

110:            else if (currentimg == nekopics[1])

111:         currentimg = nekopics[0];

112:            else currentimg = nekopics[0];

113:

114:            repaint();

115:            pause(150);

116:         }

117:      }

118:

119:      void nekoscratch(int numtimes) {

120:         for (int i = numtimes; i > 0; i --) {

121:            currentimg = nekopics[4];

122:            repaint();

123:            pause(150);

124:            currentimg = nekopics[5];

125:            repaint();

126:            pause(150);

127:      }

128:   }

129:

130:      void nekosleep(int numtimes) {

131:         for (int i = numtimes; i > 0; i --) {

132:            currentimg = nekopics[6];

133:            repaint();

134:            pause(250);

135:            currentimg = nekopics[7];

136:            repaint();

137:            pause(250);

138:      }

139:

140:      void pause(int time) {

141:         try { Thread.sleep(time); }

142:         catch (InterruptedException e) { }

143:      }

144:

145:      public void paint(Graphics g) {

146:         g.drawImage(currentimg, xpos. ypos, this);

147:      }

148:   }

Abrufen und Verwenden von Sound

Java unterstützt das Abspielen von Sound eigenständig oder in Verbindung mit laufenden Animationen. Die Unterstützung von Sound ist wie die von Bildern in den Applet- und awt-Klassen integriert, so daß die Verwendung von Sound in Java-Applets so einfach ist wie das Laden und Anzeigen von Bildern.

In der derzeitigen Version unterstützt Java für Sound nur das AU-Format von Sun, das auch µ-Format genannt wird. AU-Dateien sind in der Regel kleiner als Sound-Dateien in anderen Formaten, jedoch ist die Tonqualität nicht besonders gut. Falls Sie eine gute Tonqualität benötigen, müssen Sie Ihre Tonstreifen eventuell auf die übliche HTML-Weise (als Verknüpfungen zu externen Dateien) einbinden und nicht im Java-Applet.

Die einfachste Art, Sound abzurufen und abzuspielen, bietet die play()-Methode, die sich in der Applet-Klasse befindet und deshalb für alle Applets verfügbar ist. Die play()-Methode ist vergleichbar mit getImage, weil sie ebenfalls zwei Formen annimmt:

Zum Beispiel holt folgende Codezeile den Sound meow.au, der sich im Audio-Verzeichnis befindet, und spielt ihn ab. Das Audio-Verzeichnis befindet sich im gleichen Verzeichnis wie dieses Applet:

play(getCodeBase(), "audio/meow.au");

Die play-Methode holt den jeweiligen Sound so schnell wie möglich nach dem Aufruf und spielt ihn ab. Kann sie den angegebenen Sound nicht finden, erhalten Sie keine Fehlermeldung. Lediglich der Ton wird nicht wie erwartet abgespielt.

Möchten Sie einen Sound wiederholt abspielen, müssen Sie das Soundclip starten und stoppen oder als Schleife (immer wieder) abspielen. Das ist etwas komplizierter. In diesem Fall verwenden Sie die Applet-Methode getAudioClip(), um das Soundclip in eine Instanz der Klasse AudioClip (Teil von java.applet -Import nicht vergessen!) zu laden und sie dann direkt auf dieses AudioClip-Objekt anzuwenden.

Nehmen wir beispielsweise an, Sie haben eine Soundschleife, die im Applet-Hintergrund abgespielt werden soll. In Ihrem Initialisierungscode können Sie folgende Zeile schreiben, um das Audioclip zu holen:

AudioClip clip = getAudioClip(getCodeBase(),

   "audio/loop.au");

Dann verwenden Sie die play-Methode, um das Clip einmal abzuspielen:

clip.play();

Zum Unterbrechen eines laufenden Soundclips verwenden Sie die stop()-Methode:

clip.stop();

Um das Clip wiederholt (als Schleife) abzuspielen, verwenden Sie die loop()-Methode:

clip.loop();

Kann die getAudioClip-Methode den angegebenen Sound nicht finden oder aus irgendeinem Grund nicht laden, wird die AudioClip-Variable auf null gesetzt. Für diesen Fall sollten Sie Ihren Code testen, bevor Sie das Audioclip abspielen, da der Versuch, die Methoden play(), stop() und loop() mit einem null-Objekt aufzurufen, zu einem Fehler (eigentlich einer Ausnahme) führt.

Sie können in Ihrem Applet beliebig viele Audioclips abspielen. Alle Tonstreifen, die Sie verwenden, werden beim Ausführen des Applets gleichzeitig abgespielt.

Falls Sie Hintergrundton - ein Soundclip, das wiederholt abgespielt wird - verwenden, wird die Wiedergabe dieses Soundclips nicht automatisch gstoppt, wenn der Applet-Thread angehalten wird. Das bedeutet, daß der erste Applet-Sound weiter ertönt, auch wenn der Benutzer eine andere Seite anzeigt. Sie können dieses Problem beheben, indem Sie in der stop()-Methode den Hintergrundton des Applets stoppen:

public void stop() {

   if (runner != null) {

      bgsound.stop();

   runner.stop();

   runner = null;

   }

}

Listing 11.4 enthält einen einfachen Rahmen für ein Applet, das zwei Sounds abspielt: Der erste ist ein Hintergrundsound namens loop.au, der wiederholt abgespielt wird. De zweite ist eine Art Signalgedröhn (beep.au), das alle fünf Sekunden ertönt. (Es gibt keine Abbildung dieses Applets, weil es außer einer einfachen Zeichenkette am Bildschirm nichts anzeigt.)

Listing 11.4: Das AudioLoop-Applet

1:   import java.awt.Graphics;

2:   import java.applet.AudioClip;

3:

4:   public class AudioLoop extends java.applet.Applet

5:      implements Runnable {

6:

7:      AudioClip bgsound;

8:      AudioClip beep;

9:      Thread runner;

10:

11:      public void start() {

12:         if (runner == null) {

13:         runner = new Thread(this);

14:         runner.start();

15:      }

16:   }

17:

18:      public void stop() {

19:         if (runner != null) {

20:            if (bgsound != null) bgsound.stop();

21:            runner.stop();

22:            runner = null;

23:      }

24:   }

25:

26:      public void init() {

27:         bgsound = getAudioClip(getCodeBase(), "audio/loop.au");

28:         beep = getAudioClip(getCodeBase(), "audio/beep.au");

29:   }

30:

31:      public void run() {

32:         if (bgsound != null) bgsound.loop();

33:         while (runner != null) {

34:            try { Thread.sleep(5000); }

35:            catch (InterruptedException e) { }

36:            if (bgsound != null) beep.play();

37:      }

38:   }

39:

40:      public void paint(Graphics g) {

41:         g.drawString("Playing Sounds....", 10, 10);

42:      }

43:   }

Das Animator-Applet von Sun

Da die meisten Java-Animationen viel gemeinsamen Code enthalten, vereinfacht die Möglichkeit, diesen Code immer wieder zu verwenden, das Erstellen von Animationen mit Bildern und Ton, insbesondere für Java-Programmierer, die nicht viel Erfahrung haben. Dafür bietet Sun die Animator-Klasse als Teil der Java-Standardversion.

Das Animator-Applet bietet eine einfache allgemeine Animationsschnittstelle. Sie kompilieren den Code und erstellen eine HTML-Datei mit den entsprechenden Parametern für die Animation. Mit dem Animator-Applet können Sie:

Doch auch wenn Sie nicht beabsichtigen, den Animator-Code von Sun zu verwenden, ist es ein sehr gutes Beispiel, wie Animationen in Java funktionieren, und enthüllt einige Tricks, die Sie in Ihren Java-Applets anwenden können.

Die Animator-Klasse ist Teil der Java-Distribution (im demo-Verzeichnis). Mehr Informationen darüber erhalten Sie auch in der Java-Home-Page unter http://java.sun.com.

Double-Buffering

Gestern haben Sie zwei einfache Verfahren gelernt, um den Flimmereffekt in Java-Animationen zu verringern. Sie haben zwar spezifisch an Animationen mit Zeichnungen geübt, jedoch kann sich auch bei Animationen mit Bildern ein Flimmereffekt ergeben. Zusätzlich zu den zwei gestern beschriebenen Techniken, um diesen Flimmereffekt zu reduzieren, gibt es noch eine Möglichkeit, dies in einer Anwendung zu erreichen. Dieses Verfahren nennt man Double-Buffering.

Mit Double-Buffering erstellen Sie eine zweite Oberfläche (sozusagen außerhalb des Bildschirms), in der alles vorgezeichnet und dann auf einmal im Applet (bzw. am Bildschirm) ausgegeben wird. Das bedeutet, daß die Elemente des Applets nicht online am Bildschirm gezeichnet werden. Da die gesamte Arbeit hinter den Kulissen stattfindet, besteht keinerlei Risiko, daß Elemente nicht reibungslos ineinander übergehen oder Teile zu spät bzw. nicht synchron angezeigt werden.

Double-Buffering ist aber nicht immer die beste Lösung. Leidet Ihr Applet unter einem starken Flimmereffekt, versuchen Sie es mit Überschreiben von update und zeichnen Sie zuerst nur Teile des Bildschirms. Das kann je nach Applet die ideale Lösung sein. Double-Buffering ist weniger effizient als das übliche Puffern und nimmt auch mehr Speicherplatz in Anspruch. Wenn Sie diese Technik vermeiden können, sollten Sie dies tun. Um Flimmern in einer Animation so gut wie vollständig zu beseitigen, funktioniert das Double-Buffering aber außergewöhnlich gut.

Applets mit Double-Buffering erstellen

Um die Double-Buffering-Technik anzuwenden, brauchen Sie zwei Dinge: Ein Bild und einen Grafikbezug für dieses Bild. Zusammen simulieren diese beiden Elemente die Wirkung der Zeichnungsoberfläche des Applets. Der Grafikbezug (Graphics-Kontext, eine Instanz von Graphics) dient zur Bereitstellung der Zeichnungsmethoden, z. B. drawImage und drawString, und Image stellt die zu zeichnenden Punkte dar.

Das Einfügen von Double-Buffering in ein Applet erfolgt in vier Schritten. Erstens müssen die Offline-Oberfläche und der Grafikkontext in Instanzvariablen gespeichert werden, um sie an die paint()-Methode weitergeben zu können. Sie erklären dafür in Ihrer Klassendefinition folgende Instanzvariablen:

Image offscreenImage;

Graphics offscreenGraphics;

Zweitens müssen Sie in der Initialisierung des Applets ein Image- und ein Graphics-Objekt erstellen und sie diesen Variablen zuweisen (Sie müssen aber auf die Initialisierung warten, um zu wissen, wie groß diese Objekte sein müssen). Die createImage-Methode liefert Ihnen eine Instanz von Image, die Sie dann an die getGraphics()-Methode abgeben können, um einen neuen Grafikkontext für dieses Bild zu erhalten:

offscreenImage = createImage(this.size().width,

   this.size().height);

offscreenGraphics = offscreenImage.getGraphics();

Alles, was Sie jetzt (normalerweise in der paint-Methode) am Bildschirm ausgeben wollen, wird in die Offscreen-Grafik geschrieben. Um beispielsweise ein Bild namens img in Position 10,10 zu zeichnen, schreiben Sie diese Zeile:

offscreenGraphics.drawImage(img,10,10,this);

Schließlich fügen Sie am Ende der paint-Methode folgende Zeile ein, um den Inhalt des Offscreen-Puffers auf dem wirklichen Bildschirm auszugeben:

g.drawImage(offscreenImage, 0, 0, this);

Selbstverständlich müssen Sie update überschreiben, damit der Bildschirm zwischen den Ausgaben nicht geleert wird:

public void update(Graphics g) {

   paint(g);

}

Im folgenden die vier Schritte nochmals in der Übersicht:

Beispiel: Überarbeitung des Checkers-Applets

Im gestrigen praktischen Beispiel haben Sie eine Animation geschrieben, in der sich ein rotes Oval bewegt. Sie haben festgestellt, daß es stark flimmert und gelernt, diesen Flimmereffekt zu reduzieren. Trotz der Arbeit, die Sie gestern ausgeführt haben, flimmert das Checkers-Applet gelegentlich noch. Wir wollen dieses Applet nun überarbeiten, um die Double-Buffering-Technik anzuwenden.

Zuerst fügen Sie die Instanzvariablen für das Offscreen-Bild und den Grafikkontext ein:

Image offscreenImg;

Graphics offscreenG;

Danach fügen Sie eine init-Methode ein, um den Offscreen-Puffer zu initialisieren:

public void init() {

   offscreenImg = createImage(this.size().width,

   this.size().height);

   offscreenG = offscreenImg.getGraphics();

}

Drittens ändern Sie die paint-Methode, so daß die Ausgabe zunächst in den Offscreen-Puffer und nicht in den eigentlichen Bildschirmpuffer geschrieben wird:

public void paint(Graphics g) {

   // Hintergrund zeichnen

   offscreenG.setColor(Color.black);

   offscreenG.fillRect(0,0,100,100);

   offscreenG.setColor(Color.white);

   offscreenG.fillRect(100,0,100,100);

   // Damestein zeichnen

   offscreenG.setColor(Color.red);

   offscreenG.fillOval(xpos,5,90,90);

   g.drawImage(offscreenImg,0,0,this);

}

Beachten Sie, daß das Hauptrechteck in der update-Methode immer noch wie gestern geclippt wird. Sie brauchen diesen Teil nicht zu ändern. Der einzige relevante Teil ist die letzte paint-Methode, in der zuerst in den Offscreen-Puffer gezeichnet wird und dann erst die endgültige Bildschirmausgabe erfolgt.

Zusammenfassung

Wir haben uns in der heutigen Lektion auf drei wichtige Themen konzentriert. Erstens haben Sie gelernt, wie man Bilder in Applets verwendet - wie man sie abruft und mit der drawImage-Methode anzeigt. Sie haben gelernt, wie Bilder skaliert werden. Und Sie haben gelernt, Animationen mit Bildern zu entwickeln.

Zweitens haben Sie gelernt, wie man Sound in Applets integriert - wie Sound zu einem bestimmten Zeitpunkt oder wiederholt als Hintergrundton in Applets abgespielt werden kann. Und Sie haben gelernt, wie Sound mit der play()- und getAudioClip()-Methode abgerufen, geladen und abgespielt wird.

Drittens haben Sie gelernt, Double-Buffering in Applets anzuwenden. Mit dieser Technik können Sie den Flimmereffekt in Animationen praktisch ganz beseitigen, allerdings auf Kosten der Effizienz und Geschwindigkeit der Animation. Sie haben gelernt, Bilder und Grafikkontext zu verwenden, um alle Elemente eines Applets vorab in einen Offscreen-Puffer zu schreiben, so daß das Applet komplett am Bildschirm des Benutzers angezeigt wird.


Fragen und Antworten

F: In dem Neko-Programm haben Sie das Laden der Bilder in die init()-Methode gestellt. Mir scheint, daß es lange dauert, bis alle Bilder geladen werden. Da sich die init()-Methode nicht im Haupt-Thread des Applets befindet, entsteht doch eine spürbare Pause. Warum werden die Bilder nicht am Anfang der run()-Methode geladen?

A: Hinter den Kulissen laufen trickreiche Dinge ab. Die getImage-Methode lädt das Bild nicht, sondern gibt fast sofort ein Image-Objekt aus, so daß bei der Initialisierung nicht viel Verarbeitungsleistung verbraucht wird. Die Bilddaten, auf die getImage zeigt, werden erst geladen, wenn das Bild gebraucht wird. Auf diese Weise muß Java nicht eine große Menge Bilder im Speicher handhaben, wenn das Programm nur einen kleinen Teil davon nutzt. Statt dessen wird nur eine Referenz auf diese Daten geführt. Anhand der Referenz können die Daten dann bei Bedarf abgerufen werden.

F: Ich habe ein Applet geschrieben und mit den Methoden getAudioClip() und loop() Hintergrundsound eingefügt. Der Sound funktioniert absolut korrekt, wird aber endlos abgespielt. Ich habe versucht, den Thread abzubrechen, aber der Sound läuft weiter.

A: Ich habe diesen Aspekt in dem Abschnitt über Sound erwähnt. Hintergrundsound läuft nicht im Haupt-Thread des Applets. Das heißt, daß der Sound weiterläuft, auch wenn Sie den Thread stoppen. Die Lösung ist ganz einfach: Sie stoppen den Sound in der gleichen Methode, in der auch der Thread gestoppt wird:

runner.stop() // Thread stoppen
bgsound.stop() // Sound stoppen

F: Muß ich mein Applet auf einen kleinen Bildschirmbereich clippen, auch wenn ich die Double-Buffering-Technik anwende? Mir scheint es einfacher, jedesmal den vollen Rahmen zu zeichnen, da Double-Buffering den Flimmereffekt ausmerzt.

A: Einfacher wäre das schon, aber weniger effizient. Wird nur ein Teil des Bildschirms gezeichnet, reduziert sich nicht nur das Flimmern, sondern auch der Aufwand, den Ihr Applet in der paint()-Methode bewältigen muß. Je schneller die paint()-Methode arbeitet, um so schneller und glatter läuft Ihre Animation. Das Clipping von Bildschirmbereichen ist allgemein ein gutes Verfahren, nicht nur, um den Flimmereffekt loszuwerden.


Copyright ©1996 Markt&Technik
Buch- und Software- Verlag GmbH
Alle Rechte vorbehalten. All rights reserved.

Schreiben Sie uns!

Previous Page TOC Index Next Page See Page