Tit   Inh   Ind   1   2   3   4   5   6   7   8   9   10   11   12   13   14   15   16   17   18   19   20   21   22   23   24   25   26   27   28   29   30   31   32   <<   <   >   >> 

24.2 Animation



24.2.1 Prinzipielle Vorgehensweise

Das Darstellen einer Animation auf dem Bildschirm ist im Prinzip nichts anderes als die schnell aufeinanderfolgende Anzeige einer Sequenz von Einzelbildern. Die Bildfolge erscheint dem menschlichen Auge aufgrund seiner Trägheit als zusammenhängende Bewegung.

Obwohl die prinzipielle Vorgehensweise damit klar umrissen ist, steckt die Tücke bei der Darstellung von animierten Bildsequenzen im Detail. Zu den Problemen, die in diesem Zusammenhang zu lösen sind, gehören:

All dies sind Standardprobleme, die vom Programmierer bei der Entwicklung von Animationen zu lösen sind. Wir werden feststellen, daß Java dafür durchweg brauchbare Lösungen zu bieten hat und die Programmierung kleiner Animationen recht einfach zu realisieren ist.

Die repaint-Schleife

Das Grundprinzip einer Animation besteht darin, in einer Schleife die Methode repaint wiederholt aufzurufen. Ein Aufruf von repaint führt dazu, daß die paint-Methode aufgerufen wird, und innerhalb von paint generiert die Anwendung dann die für das aktuelle Einzelbild benötigte Bildschirmausgabe.

paint muß sich also merken (oder mitgeteilt bekommen), welches Bild bei welchem Aufruf erzeugt werden soll. Typischerweise wird dazu ein Schleifenzähler verwendet, der das gerade anzuzeigende Bild bezeichnet. Nach dem Ausführen der Ausgabeanweisungen terminiert paint, und der Aufrufer wartet eine bestimmte Zeitspanne. Dann zählt er den Bildzähler hoch und führt den nächsten Aufruf von repaint durch. Dies setzt sich so lange fort, bis die Animation beendet ist oder das Programm abgebrochen wird.

Das folgende Listing stellt eines der einfachsten Beispiele für eine Grafikanimation dar:

001 /* Listing2406.java */
002 
003 import java.awt.*;
004 import java.awt.event.*;
005 
006 public class Listing2406
007 extends Frame
008 {
009    int cnt = 0;
010 
011    public static void main(String[] args)
012    {
013       Listing2406 wnd = new Listing2406();
014       wnd.setSize(250,150);
015       wnd.setVisible(true);
016       wnd.startAnimation();
017    }
018 
019    public Listing2406()
020    {
021       super("Listing2406");
022       setBackground(Color.lightGray);
023       //WindowListener
024       addWindowListener(
025          new WindowAdapter() {
026             public void windowClosing(WindowEvent event)
027             {
028                System.exit(0);
029             }
030          }
031       );
032    }
033 
034    public void startAnimation()
035    {
036       while (true) {
037          repaint();
038       }
039    }
040 
041    public void paint(Graphics g)
042    {
043       ++cnt;
044       g.drawString("Counter = "+cnt,10,50);
045       try {
046          Thread.sleep(1000);
047       } catch (InterruptedException e) {
048       }
049    }
050 }
Listing2406.java
Listing 24.6: Ein animierter Zähler

 Beispiel 

Das Programm öffnet ein Fenster und zählt in Sekundenabständen einen Zähler um eins hoch:

Abbildung 24.3: Ein animierter Zähler

Leider hat das Programm einen entscheidenden Nachteil. Die Animation selbst funktioniert zwar wunderbar, aber das Programm reagiert nur noch sehr schleppend auf Windows-Nachrichten. Wir wollen zunächst dieses Problem abstellen und uns ansehen, wie man die repaint-Schleife in einem eigenen Thread laufen läßt.

 Warnung 

Verwendung von Threads

Um die vorherige Version des Programms zu verbessern, sollte die repaint-Schleife in einem eigenen Thread laufen. Zusätzlich ist es erforderlich, die Zeitverzögerung aus paint herauszunehmen und statt dessen in die repaint-Schleife zu verlagern. So bekommt der Haupt-Thread des Animationsprogramms genügend Zeit, die Bildschirmausgabe durchzuführen, und kann andere Events bearbeiten. Daß in einem anderen Thread eine Endlosschleife läuft, merkt er nur noch daran, daß in regelmäßigen Abständen repaint-Ereignisse eintreffen.

Um das Programm auf die Verwendung mehrerer Threads umzustellen, sollte die Fensterklasse das Interface Runnable implementieren und eine Instanzvariable vom Typ Thread anlegen. Dann wird die Methode startAnimation so modifiziert, daß sie den neuen Thread instanziert und startet. Die eigentliche repaint-Schleife wird in die Methode run verlagert. Schließlich sollte beim Beenden des Programms auch der laufende Thread beendet werden. Hier ist die modifizierte Fassung:

001 /* Listing2407.java */
002 
003 import java.awt.*;
004 import java.awt.event.*;
005 
006 public class Listing2407
007 extends Frame
008 implements Runnable
009 {
010    int cnt = 0;
011 
012    public static void main(String[] args)
013    {
014       Listing2407 wnd = new Listing2407();
015       wnd.setSize(250,150);
016       wnd.setVisible(true);
017       wnd.startAnimation();
018    }
019 
020    public Listing2407()
021    {
022       super("Listing2407");
023       setBackground(Color.lightGray);
024       //WindowListener
025       addWindowListener(
026          new WindowAdapter() {
027             public void windowClosing(WindowEvent event)
028             {
029                System.exit(0);
030             }
031          }
032       );
033    }
034 
035    public void startAnimation()
036    {
037       Thread th = new Thread(this);
038       th.start();
039    }
040 
041    public void run()
042    {
043       while (true) {
044          repaint();
045          try {
046             Thread.sleep(1000);
047          } catch (InterruptedException e) {
048             //nichts
049          }
050       }
051    }
052 
053    public void paint(Graphics g)
054    {
055       ++cnt;
056       g.drawString("Counter = "+cnt,10,50);
057    }
058 }
Listing2407.java
Listing 24.7: Verwendung von Threads zur Animation

Das so modifizierte Programm erzeugt dieselbe Ausgabe wie das vorige, ist aber in der Lage, in der gewohnten Weise auf Ereignisse zu reagieren. Selbst wenn die Verzögerungsschleife ganz entfernt und der Hauptprozeß so pausenlos mit repaint-Anforderungen bombardiert würde, könnte das Programm noch normal beendet werden.

24.2.2 Abspielen einer Folge von Bitmaps

Eine der einfachsten und am häufigsten verwendeten Möglichkeiten, eine Animation zu erzeugen, besteht darin, die zur Darstellung erforderliche Folge von Bitmaps aus einer Reihe von Bilddateien zu laden. Jedem Einzelbild wird dabei ein Image-Objekt zugeordnet, das vor dem Start der Animation geladen wird. Alle Images liegen in einem Array oder einem anderen Container und werden in der repaint-Schleife nacheinander angezeigt.

Das folgende Programm speichert die 30 anzuzeigenden Einzelbilder in einem Array arImg, das nach dem Start des Programms komplett geladen wird. Da dieser Vorgang einige Sekunden dauern kann, zeigt das Programm den Ladefortschritt auf dem Bildschirm an:

Abbildung 24.4: Die Ausgabe während des Ladevorgangs

Erst nach dem vollständigen Abschluß des Ladevorgangs, der mit einem MediaTracker überwacht wird, beginnt die eigentliche Animation. Die ganzzahlige Instanzvariable actimage dient als Zähler für die Bildfolge und wird nacheinander von 0 bis 29 hochgezählt, um dann wieder bei 0 zu beginnen. Nach jedem Einzelbild wartet das Programm 50 Millisekunden und führt dann den nächsten Aufruf von repaint durch:

001 /* Listing2408.java */
002 
003 import java.awt.*;
004 import java.awt.event.*;
005 
006 public class Listing2408
007 extends Frame
008 implements Runnable
009 {
010    Thread th;
011    Image arImg[];
012    int actimage;
013 
014    public static void main(String[] args)
015    {
016       Listing2408 wnd = new Listing2408();
017       wnd.setSize(200,150);
018       wnd.setVisible(true);
019       wnd.startAnimation();
020    }
021 
022    public Listing2408()
023    {
024       super("Listing2408");
025       //WindowListener
026       addWindowListener(
027          new WindowAdapter() {
028             public void windowClosing(WindowEvent event)
029             {
030                System.exit(0);
031             }
032          }
033       );
034    }
035 
036    public void startAnimation()
037    {
038       th = new Thread(this);
039       actimage = -1;
040       th.start();
041    }
042 
043    public void run()
044    {
045       //Bilder laden
046       arImg = new Image[30];
047       MediaTracker mt = new MediaTracker(this);
048       Toolkit tk = getToolkit();
049       for (int i = 1; i <= 30; ++i) {
050          arImg[i-1] = tk.getImage("images/jana"+i+".gif");
051          mt.addImage(arImg[i-1], 0);
052          actimage = -i;
053          repaint();
054          try {
055             mt.waitForAll();
056          } catch (InterruptedException e) {
057             //nothing
058          }
059       }
060       //Animation beginnen
061       actimage = 0;
062       while (true) {
063          repaint();
064          actimage = (actimage + 1) % 30;
065          try {
066             Thread.sleep(50);
067          } catch (InterruptedException e) {
068             //nichts
069          }
070       }
071    }
072 
073    public void paint(Graphics g)
074    {
075       if (actimage < 0) {
076          g.drawString("Lade Bitmap "+(-actimage),10,50);
077       } else {
078          g.drawImage(arImg[actimage],10,30,this);
079       }
080    }
081 }
Listing2408.java
Listing 24.8: Abspielen einer Folge von Bitmaps

 Beispiel 

Das vorliegende Beispiel verwendet die Bilddateien jana1.gif bis jana30.gif. Sie zeigen die verschiedenen Phasen des in Schreibschrift geschriebenen Namens »Jana«. Alternativ kann aber auch jede andere Sequenz von Bilddateien verwendet werden. Die folgenden Abbildungen zeigen einige Schnappschüsse der Programmausgabe:

 Hinweis 

Abbildung 24.5: Animation eines Schriftzugs, Schnappschuß 1

Abbildung 24.6: Animation eines Schriftzugs, Schnappschuß 2

Abbildung 24.7: Animation eines Schriftzugs, Schnappschuß 3

24.2.3 Animation mit Grafikprimitiven

Alternativ zur Anzeige von Bilddateien kann jedes Einzelbild der Animation natürlich auch mit den Ausgabeprimitven der Klasse Graphics erzeugt werden. Dies hat den Vorteil, daß der Anwender nicht auf das Laden der Bilder warten muß. Außerdem ist das Verfahren flexibler als der bitmap-basierte Ansatz. Der Nachteil ist natürlich, daß die Grafikoperationen zeitaufwendiger sind und eine zügige Bildfolge bei komplexen Sequenzen schwieriger zu erzielen ist.

Als Beispiel für diese Art von Animation wollen wir uns die Aufgabe stellen, eine aus rechteckigen Kästchen bestehende bunte Schlange über den Bildschirm laufen zu lassen. Sie soll an den Bildschirmrändern automatisch umkehren und auch innerhalb des Ausgabefensters von Zeit zu Zeit ihre Richtung wechseln.

Das folgende Programm stellt die Schlange als Vector von Objekten des Typs ColorRectangle dar. ColorRectangle ist aus Rectangle abgeleitet und besitzt zusätzlich eine Membervariable zur Darstellung der Farbe des Rechtecks.

Dieses Beispiel folgt dem allgemeinen Architekturschema für Animationen, das wir auch in den letzten Beispielen verwendet haben. Der erste Schritt innerhalb von run besteht darin, die Schlange zu konstruieren. Dazu wird eine Folge von Objekten der Klasse ColorRectangle konstruiert, und ab Position (100,100) werden die Objekte horizontal nebeneinander angeordnet. Die Farben werden dabei so vergeben, daß die Schlange in fließenden Übergängen von rot bis blau dargestellt wird. Alle Elemente werden in dem Vector snake gespeichert.

Nachdem die Schlange konstruiert wurde, beginnt die Animation. Dazu wird die aktuelle Schlange angezeigt, eine Weile pausiert und dann durch Aufruf der Methode moveSnake die nächste Position der Schlange berechnet. moveSnake ist relativ aufwendig, denn hier liegt der Löwenanteil der »Intelligenz« der Animation. Die Richtung der Bewegung der Schlange wird durch die Variablen dx und dy getrennt für die x- und y-Richtung bestimmt. Steht hier der Wert -1, bewegt sich die Schlange im nächsten Schritt um die Breite eines Rechtecks in Richtung kleinerer Koordinaten. Bei 1 vergrößert sie die Koordinate entsprechend, und wenn der Wert 0 enthalten ist, verändert sich der zugehörige Koordinatenwert im nächsten Schritt gar nicht.

dx und dy werden entweder dann verändert, wenn die Schlange an einem der vier Bildschirmränder angekommen ist und umkehren muß oder (im Mittel bei jedem zehnten Schritt) auch auf freier Strecke. Nachdem auf diese Weise die neue Richtung bestimmt wurde, wird das erste Element der Schlange auf die neue Position bewegt. Alle anderen Elemente der Schlange bekommen dann die Position zugewiesen, die zuvor ihr Vorgänger hatte.

Eine alternative Art, die Schlange neu zu berechnen, würde darin bestehen, lediglich ein neues erstes Element zu generieren, an vorderster Stelle in den Vector einzufügen und das letzte Element zu löschen. Dies hätte allerdings den Nachteil, daß die Farbinformationen von vorne nach hinten durchgereicht würden und so jedes Element seine Farbe ständig ändern würde. Dieses (sehr viel performantere) Verfahren könnte verwendet werden, wenn alle Elemente der Schlange dieselbe Farbe hätten.

Hier ist der Quellcode zu der Schlangenanimation:

001 /* Listing2409.java */
002 
003 import java.awt.*;
004 import java.awt.event.*;
005 import java.util.*;
006 
007 class ColorRectangle
008 extends Rectangle
009 {
010    public Color color;
011 }
012 
013 public class Listing2409
014 extends Frame
015 implements Runnable
016 {
017    //Konstanten
018    private static final int   SIZERECT    = 7;
019    private static final int   SLEEP       = 40;
020    private static final int   NUMELEMENTS = 20;
021    private static final Color BGCOLOR     = Color.black;
022 
023    //Instanzvariablen
024    private Thread th;
025    private Vector snake;
026    private int dx;
027    private int dy;
028 
029    public static void main(String args[])
030    {
031       Listing2409 frame = new Listing2409();
032       frame.setSize(200,150);
033       frame.setVisible(true);
034       frame.startAnimation();
035    }
036 
037    public Listing2409()
038    {
039       super("Listing2409");
040       setBackground(BGCOLOR);
041       //WindowListener
042       addWindowListener(
043          new WindowAdapter() {
044             public void windowClosing(WindowEvent event)
045             {
046                System.exit(0);
047             }
048          }
049       );
050       snake = new Vector();
051    }
052 
053    public void startAnimation()
054    {
055       th = new Thread(this);
056       th.start();
057    }
058 
059    public void run()
060    {
061       //Schlange konstruieren
062       ColorRectangle cr;
063       int x = 100;
064       int y = 100;
065       for (int i=0; i < NUMELEMENTS; ++i) {
066          cr = new ColorRectangle();
067          cr.x = x;
068          cr.y = y;
069          cr.width = SIZERECT;
070          cr.height = SIZERECT;
071          x += SIZERECT;
072          cr.color = new Color(
073             i*(256/NUMELEMENTS),
074             0,
075             240-i*(256/NUMELEMENTS)
076          );
077          snake.addElement(cr);
078       }
079 
080       //Vorzugsrichtung festlegen
081       dx = -1;
082       dy = -1;
083 
084       //Schlange laufen lassen
085       while (true) {
086          repaint();
087          try {
088             Thread.sleep(SLEEP);
089          } catch (InterruptedException e){
090             //nichts
091          }
092          moveSnake();
093       }
094    }
095 
096    public void moveSnake()
097    {
098       Dimension size = getSize();
099       int sizex = size.width-getInsets().left-getInsets().right;
100       int sizey = size.height-getInsets().top-getInsets().bottom;
101       ColorRectangle cr = (ColorRectangle)snake.firstElement();
102       boolean lBorder = false;
103       int xalt, yalt;
104       int xtmp, ytmp;
105 
106       //Kopf der Schlange neu berechnen
107       if (cr.x <= 1) {
108          dx = 1;
109          lBorder = true;
110       }
111       if (cr.x + cr.width >= sizex) {
112          dx = -1;
113          lBorder = true;
114       }
115       if (cr.y <= 1) {
116          dy = 1;
117          lBorder = true;
118       }
119       if (cr.y + cr.height >= sizey) {
120          dy = -1;
121          lBorder = true;
122       }
123       if (! lBorder) {
124          if (rand(10) == 0) {
125             if (rand(2) == 0) {
126                switch (rand(5)) {
127                case 0: case 1:
128                   dx = -1;
129                   break;
130                case 2:
131                   dx = 0;
132                   break;
133                case 3: case 4:
134                   dx = 1;
135                   break;
136                }
137             } else {
138                switch (rand(5)) {
139                case 0: case 1:
140                   dy = -1;
141                   break;
142                case 2:
143                   dy = 0;
144                   break;
145                case 3: case 4:
146                   dy = 1;
147                   break;
148                }
149             }
150          }
151       }
152       xalt = cr.x + SIZERECT * dx;
153       yalt = cr.y + SIZERECT * dy;
154       //Rest der Schlange hinterherziehen
155       Enumeration e = snake.elements();
156       while (e.hasMoreElements()) {
157          cr = (ColorRectangle)e.nextElement();
158          xtmp = cr.x;
159          ytmp = cr.y;
160          cr.x = xalt;
161          cr.y = yalt;
162          xalt = xtmp;
163          yalt = ytmp;
164       }
165    }
166 
167    public void paint(Graphics g)
168    {
169       ColorRectangle cr;
170       Enumeration e = snake.elements();
171       int inleft    = getInsets().left;
172       int intop     = getInsets().top;
173       while (e.hasMoreElements()) {
174          cr = (ColorRectangle)e.nextElement();
175          g.setColor(cr.color);
176          g.fillRect(cr.x+inleft,cr.y+intop,cr.width,cr.height);
177       }
178    }
179 
180    private int rand(int limit)
181    {
182       return (int)(Math.random() * limit);
183    }
184 }
Listing2409.java
Listing 24.9: Die animierte Schlange

 Beispiel 

Die Schlange kann in einem beliebig kleinen oder großen Fenster laufen. Hier sind ein paar Beispiele für die Ausgabe des Programms, nachdem das Fenster in der Größe verändert wurde:

 Hinweis 

Abbildung 24.8: Die animierte Schlange, Schnappschuß 1

Abbildung 24.9: Die animierte Schlange, Schnappschuß 2

Abbildung 24.10: Die animierte Schlange, Schnappschuß 3

24.2.4 Reduktion des Bildschirmflackerns

Alle bisher entwickelten Animationen zeigen während der Ausführung ein ausgeprägtes Flackern, das umso stärker ist, je später ein Bildanteil innerhalb eines Animationsschrittes angezeigt wird. Der Grund für dieses Flackern liegt darin, daß vor jedem Aufruf von paint zunächst das Fenster gelöscht wird und dadurch unmittelbar vor der Ausgabe des nächsten Bildes ganz kurz ein vollständig leerer Hintergrund erscheint.

Leider besteht die Lösung für dieses Problem nicht einfach darin, das Löschen zu unterdrücken. Bei einer animierten Bewegung beispielsweise ist es erforderlich, all die Bestandteile des vorigen Bildes zu löschen, die im aktuellen Bild nicht mehr oder an einer anderen Stelle angezeigt werden.

Auch wenn paint deshalb aufgerufen wird, weil ein bisher verdeckter Bildausschnitt wieder sichtbar wird, muß natürlich der entsprechende Bildausschnitt zunächst gelöscht werden, um die Bestandteile des anderen Fensters zu entfernen. Im Grunde ist es also eine ganz vernünftige Vorgehensweise, das Fenster vor jedem Aufruf von paint zu löschen.

Das Flackern kann nun auf unterschiedliche Weise unterdrückt werden. Die drei gebräuchlichsten Methoden sind folgende:

Jedes dieser Verfahren hat Vor- und Nachteile und kann in verschiedenen Situationen unterschiedlich gut angewendet werden. Wir werden sie in den folgenden Unterabschnitten kurz vorstellen und ein Beispiel für ihre Anwendung geben. Es gibt noch einige zusätzliche Möglichkeiten, das Flackern zu unterdrücken oder einzuschränken, wie beispielsweise das Clipping der Ausgabe auf den tatsächlich veränderten Bereich, aber darauf wollen wir hier nicht näher eingehen.

Bildschirm nicht löschen

Den Bildschirm überhaupt nicht zu löschen, um das Flackern zu unterdrücken, ist nur bei nicht bewegten Animationen möglich. Wir wollen uns als Beispiel für ein Programm, das hierfür geeignet ist, das folgende Lauflicht ansehen:

001 /* Listing2410.java */
002 
003 import java.awt.*;
004 import java.awt.event.*;
005 
006 public class Listing2410
007 extends Frame
008 implements Runnable
009 {
010    //Konstanten
011    private static final int NUMLEDS  = 20;
012    private static final int SLEEP    = 60;
013    private static final int LEDSIZE  = 10;
014    private static final Color ONCOLOR  = new Color(255,0,0);
015    private static final Color OFFCOLOR = new Color(100,0,0);
016 
017    //Instanzvariablen
018    private Thread th;
019    private int switched;
020    private int dx;
021 
022    public static void main(String args[])
023    {
024       Listing2410 frame = new Listing2410();
025       frame.setSize(270,150);
026       frame.setVisible(true);
027       frame.startAnimation();
028    }
029 
030    public Listing2410()
031    {
032       super("Listing2410");
033       setBackground(Color.lightGray);
034       //WindowListener
035       addWindowListener(
036          new WindowAdapter() {
037             public void windowClosing(WindowEvent event)
038             {
039                setVisible(false);
040                dispose();
041                System.exit(0);
042             }
043          }
044       );
045    }
046 
047    public void startAnimation()
048    {
049       th = new Thread(this);
050       th.start();
051    }
052 
053    public void run()
054    {
055       switched = -1;
056       dx = 1;
057       while (true) {
058          repaint();
059          try {
060             Thread.sleep(SLEEP);
061          } catch (InterruptedException e){
062             //nichts
063          }
064          switched += dx;
065          if (switched < 0 || switched > NUMLEDS - 1) {
066             dx = -dx;
067             switched += 2*dx;
068          }
069       }
070    }
071 
072    public void paint(Graphics g)
073    {
074       for (int i = 0; i < NUMLEDS; ++i) {
075          g.setColor(i == switched ? ONCOLOR : OFFCOLOR);
076          g.fillOval(10+i*(LEDSIZE+2),80,LEDSIZE,LEDSIZE);
077       }
078    }
079 }
Listing2410.java
Listing 24.10: Bildschirmflackern reduzieren bei stehenden Animationen

Das Programm zeigt eine Kette von 20 Leuchtdioden, die nacheinander an- und ausgeschaltet werden und dadurch ein Lauflicht simulieren, das zwischen linkem und rechtem Rand hin- und herläuft:

 Hinweis 

Abbildung 24.11: Die Lauflicht-Animation

Wie kann nun aber das Löschen verhindert werden? Die Lösung basiert auf der Tatsache, daß bei einem Aufruf von repaint nicht gleich paint, sondern zunächst die Methode update aufgerufen wird. In der Standardversion der Klasse Component könnte update etwa so implementiert sein:

001 public void update(Graphics g) 
002 {
003    g.setColor(getBackground());
004    g.fillRect(0, 0, width, height);
005    g.setColor(getForeground());
006    paint(g);
007 }
Listing 24.11: Standard-Implementierung von update

Zunächst wird die aktuelle Hintergrundfarbe ausgewählt, um in dieser Farbe ein ausgefülltes Rechteck in der Größe des Bildschirms zu zeichnen. Erst nach diesem Löschvorgang wird die Vordergrundfarbe gesetzt und paint aufgerufen.

Da in Java alle Methodenaufrufe dynamisch gebunden werden, kann das Löschen dadurch verhindert werden, daß update durch eine eigene Version überlagert wird, die den Hintergrund unverändert läßt. Durch einfaches Hinzufügen der folgenden drei Zeilen kann das Flackern des Lauflichts vollkommen unterdrückt werden:

001 /* update1.inc */
002 
003 public void update(Graphics g)
004 {
005    paint(g);
006 }
update1.inc
Listing 24.12: Modifizierte Version von update

Nur den wirklich benötigten Teil des Bildschirms löschen

Wie schon erwähnt, kann auf das Löschen des Bildschirms nur dann komplett verzichtet werden, wenn die Animation keine Bewegung enthält. Ist sie dagegen bewegt, kann es sinnvoll sein, nur die Teile des Bildes zu löschen, die beim aktuellen Animationsschritt leer sind, im vorigen Schritt aber Grafikelemente enthielten.

Um welche Teile der Grafik es sich dabei handelt, ist natürlich von der Art der Animation abhängig. Zudem muß jeder Animationsschritt Informationen über den vorigen Schritt haben, um die richtigen Stellen löschen zu können. Ein Beispiel, bei dem diese Technik gut angewendet werden kann, ist die bunte Schlange aus dem Abschnitt »Animation mit Grafikprimitiven«.

Da die Schlange bei jedem Schritt einen neuen Kopf bekommt und alle anderen Elemente die Plätze ihres jeweiligen Vorgängers einnehmen, bleibt als einziges wirklich zu löschendes Element das letzte Element der Schlange aus dem vorherigen Animationsschritt übrig. Dessen Position könnte man sich bei jedem Schritt merken und im nächsten Schritt in der Hintergrundfarbe neu zeichnen.

Noch einfacher geht es, indem man an die Schlange einfach ein zusätzliches unsichtbares Element anhängt. Wird nämlich das letzte Element grundsätzlich in der Hintergrundfarbe dargestellt, hinterläßt es keine Spuren auf dem Bildschirm und braucht damit auch nicht explizit gelöscht zu werden! Wir brauchen also nur hinter die for-next-Schleife zur Konstruktion der Schlange ein weiteres, unsichtbares Element an den snake-Vector anzuhängen (in Listing 24.13 in den Zeilen 025 bis 031 eingefügt):

001 /* Schlange2.inc */
002 
003 public void run()
004 {
005    //Schlange konstruieren
006    ColorRectangle cr;
007    int x = 100;
008    int y = 100;
009    for (int i=0; i < NUMELEMENTS; ++i) {
010       cr = new ColorRectangle();
011       cr.x = x;
012       cr.y = y;
013       cr.width = SIZERECT;
014       cr.height = SIZERECT;
015       x += SIZERECT;
016       cr.color = new Color(
017          i*(256/NUMELEMENTS),
018          0,
019          240-i*(256/NUMELEMENTS)
020       );
021       snake.addElement(cr);
022    }
023 
024    //Löschelement anhängen
025    cr = new ColorRectangle(); 
026    cr.x = x;
027    cr.y = y;
028    cr.width = SIZERECT;
029    cr.height = SIZERECT;
030    cr.color = BGCOLOR;
031    snake.addElement(cr); 
032 
033    //Vorzugsrichtung festlegen
034    dx = -1;
035    dy = -1;
036 
037    //Schlange laufen lassen
038    while (true) {
039       repaint();
040       try {
041          Thread.sleep(SLEEP);
042       } catch (InterruptedException e){
043          //nichts
044       }
045       moveSnake();
046    }
047 }
Schlange2.inc
Listing 24.13: Modifizierte Schlangenanimation

Wird nun zusätzlich die Methode update überlagert, wie es auch im vorigen Abschnitt getan wurde, läuft die Schlange vollkommen flackerfrei.

Doppelpufferung

Das Doppelpuffern bietet sich immer dann an, wenn die beiden vorigen Methoden versagen. Das kann beispielsweise dann der Fall sein, wenn es bei einer bewegten Animation zu aufwendig ist, nur den nicht mehr benötigten Teil der Bildschirmausgabe zu löschen, oder wenn der aktuelle Animationsschritt keine Informationen darüber besitzt, welcher Teil zu löschen ist.

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:

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.

Die Anwendung des Doppelpufferns ist nicht immer sinnvoll. Sollte eine der anderen Methoden mit vertretbarem Aufwand implementiert werden können, kann es sinnvoller sein, diese zu verwenden. Nachteilig sind vor allem der Speicherbedarf für die Konstruktion des Offscreen-Images und die Verzögerungen durch das doppelte Schreiben der Bilddaten. Hier muß im Einzelfall entschieden werden, welche Variante zum Einsatz kommen soll. In vielen Fällen allerdings können die genannten Nachteile vernachlässigt werden, und die Doppelpufferung ist ein probates Mittel, um das Bildschirmflackern zu verhindern.

 Tip 

Das folgende Programm ist ein Beispiel für die Anwendung des Doppelpufferns bei der Ausgabe einer bewegten Animation. Wir wollen uns dafür die Aufabe stellen, eine große Scheibe über den Bildschirm laufen zu lassen, über deren Rand zwei stilisierte »Ameisen« mit unterschiedlicher Geschwindigkeit in entgegengesetzte Richtungen laufen.

Das folgende Programm löst diese Aufgabe. Dabei folgt die Animation unserem bekannten Architekturschema für bewegte Grafik und braucht hier nicht weiter erklärt zu werden. Um das Flackern zu verhindern, deklarieren wir zwei Instanzvariablen, dbImage und dbGraphics:

private Image dbImage;
private Graphics dbGraphics;
 Beispiel 

Glücklicherweise können die zum Doppelpuffern erforderlichen Schritte gekapselt werden, wenn man die Methode update geeignet überlagert:

001 /* update2.inc */
002 
003 public void update(Graphics g)
004 {
005    //Double-Buffer initialisieren
006    if (dbImage == null) {
007       dbImage=createImage(this.size().width,this.size().height);
008       dbGraphics = dbImage.getGraphics();
009    }
010    //Hintergrund löschen
011    dbGraphics.setColor(getBackground());
012    dbGraphics.fillRect(0,0,this.size().width,this.size().height);
013    //Vordergrund zeichnen
014    dbGraphics.setColor(getForeground());
015    paint(dbGraphics);
016    //Offscreen anzeigen
017    g.drawImage(dbImage,0,0,this);
018 }
update2.inc
Listing 24.14: update-Methode mit Doppelpufferung

 Hinweis 

Falls nicht schon geschehen, werden hier zunächst die beiden Variablen dbImage und dbGraphics initialisiert. Anschließend wird der Hintergrund gelöscht, wie es auch in der Standardversion von update der Fall ist. Im Gegensatz zu dieser erfolgt das Löschen aber auf dem Offscreen-Image und ist somit für den Anwender nicht zu sehen. Nun wird paint aufgerufen und bekommt anstelle des normalen den Offscreen-Grafikkontext übergeben. Ohne selbst etwas davon zu wissen, sendet paint damit alle seine Grafikbefehle auf das Offscreen-Image. Nachdem paint beendet wurde, wird durch Aufruf von drawImage das Offscreen-Image auf dem Bildschirm angezeigt.

Hier ist der komplette Quellcode des Programms:

001 /* Listing2415.java */
002 
003 import java.awt.*;
004 import java.awt.event.*;
005 
006 public class Listing2415
007 extends Frame
008 implements Runnable
009 {
010    private Thread th;
011    private int actx;
012    private int dx;
013    private int actarc1;
014    private int actarc2;
015    private Image dbImage;
016    private Graphics dbGraphics;
017 
018    public static void main(String[] args)
019    {
020       Listing2415 frame = new Listing2415();
021       frame.setSize(210,170);
022       frame.setVisible(true);
023       frame.startAnimation();
024    }
025 
026    public Listing2415()
027    {
028       super("Listing2415");
029       //WindowListener
030       addWindowListener(
031          new WindowAdapter() {
032             public void windowClosing(WindowEvent event)
033             {
034                setVisible(false);
035                dispose();
036                System.exit(0);
037             }
038          }
039       );
040    }
041 
042    public void startAnimation()
043    {
044       Thread th = new Thread(this);
045       th.start();
046    }
047 
048    public void run()
049    {
050       actx = 0;
051       dx = 1;
052       actarc1 = 0;
053       actarc2 = 0;
054       while (true) {
055          repaint();
056          actx += dx;
057          if (actx < 0 || actx > 100) {
058             dx = -dx;
059             actx += 2*dx;
060          }
061          actarc1 = (actarc1 + 1) % 360;
062          actarc2 = (actarc2 + 2) % 360;
063          try {
064             Thread.sleep(40);
065          } catch (InterruptedException e) {
066             //nichts
067          }
068       }
069    }
070 
071    public void update(Graphics g)
072    {
073       //Double-Buffer initialisieren
074       if (dbImage == null) {
075          dbImage = createImage(
076             this.getSize().width,
077             this.getSize().height
078          );
079          dbGraphics = dbImage.getGraphics();
080       }
081       //Hintergrund löschen
082       dbGraphics.setColor(getBackground());
083       dbGraphics.fillRect(
084          0,
085          0,
086          this.getSize().width,
087          this.getSize().height
088        );
089       //Vordergrund zeichnen
090       dbGraphics.setColor(getForeground());
091       paint(dbGraphics);
092       //Offscreen anzeigen
093       g.drawImage(dbImage,0,0,this);
094    }
095 
096    public void paint(Graphics g)
097    {
098       int xoffs = getInsets().left;
099       int yoffs = getInsets().top;
100       g.setColor(Color.lightGray);
101       g.fillOval(xoffs+actx,yoffs+20,100,100);
102       g.setColor(Color.red);
103       g.drawArc(xoffs+actx,yoffs+20,100,100,actarc1,10);
104       g.drawArc(xoffs+actx-1,yoffs+19,102,102,actarc1,10);
105       g.setColor(Color.blue);
106       g.drawArc(xoffs+actx,yoffs+20,100,100,360-actarc2,10);
107       g.drawArc(xoffs+actx-1,yoffs+19,102,102,360-actarc2,10);
108    }
109 }
Listing2415.java
Listing 24.15: Animation mit Doppelpufferung

Ein Schnappschuß des laufenden Programms sieht so aus (die beiden »Ameisen« sind in der Abbildung etwas schwer zu erkennen, im laufenden Programm sieht man sie besser):

 Hinweis 

Abbildung 24.12: Eine Animation mit Doppelpufferung

Durch die Kapselung des Doppelpufferns können Programme sogar nachträglich flackerfrei gemacht werden, ohne daß in den eigentlichen Ausgaberoutinen irgend etwas geändert werden müßte. Man könnte beispielsweise aus Frame eine neue Klasse DoubleBufferFrame ableiten, die die beiden privaten Membervariablen dbImage und dbGraphics besitzt und update in der beschriebenen Weise implementiert. Alle Klassen, die dann von DoubleBufferFrame anstelle von Frame abgeleitet werden, unterstützen das Doppelpuffern ihrer Grafikausgaben automatisch.


 Tit   Inh   Ind   1   2   3   4   5   6   7   8   9   10   11   12   13   14   15   16   17   18   19   20   21   22   23   24   25   26   27   28   29   30   31   32   <<   <   >   >> 
Go To Java 2, Addison Wesley, Version 1.0.2, © 1999 Guido Krüger, http://www.gkrueger.com