Animationen sind für viele Hobby-Programmierer ein beliebtes Tätigkeitsfeld, insbesondere bei der Spieleprogrammierung. In diesem Artikel zeige ich Ihnen einige Grundprinzipien sowie Umgehungen von Hürden, damit Ihre Animationen völlig ruckelfrei, flimmerfrei und insbesondere von der CPU-Rechenleistung unabhängig auf jedem Rechner genau gleich schnell laufen.
Das Ganze finden Sie in der Billard-Simulation praktisch angewendet vor.
Als Beispiel wollen wir drei Planetenkreise mit einem siebenzackigen Stern in der Mitte darstellen:
Unsere Beispielanimation in diesem Artikel
Dabei sollen die Planeten genau 4 Sekunden für eine Umkreisung im Gegenuhrzeigersinn benötigen, während der Stern eine Umdrehung in 2 Sekunden im Uhrzeigersinn zurücklegt.
Das Grundprinzip jeder flimmerfreien Animation bildet die Verwendung von
mehreren Bildschirmspeichern. Microsoft BASIC unter MS-DOS bietet
Ihnen dazu bei den EGA-Grafikmodi mehrere solche Bildschirmseiten an, welche
Sie mit SCREEN
setzen können. Das Wichtigste ist also immer
das Zeichnen auf einer hintergründigen Bildschirmseite.
Um die Animation bezüglich Rechenzeit zu optimieren, bietet es sich an,
die statischen Teile der Grafik in einer weiteren
Bildschirmpufferseite zu zeichnen, so dass diese für jedes neue Bild
direkt mit PCOPY
umkopiert werden kann:
' Animationsdemonstration Planetenkreise mit Stern ' Ansatz 1 Pi! = 4! * ATN(1!) SCREEN 7, , 4, 4 CIRCLE (160, 100), 118, 15, , , 5! / 6! PAINT (160, 100), 15
Statische Grundszene der Beispielanimation
Bei der Billard-Simulation bildet die Bande diese statische Szene.
Für die bewegten Partien sollten Sie einen Programmteil schreiben, welcher Ihnen Ihre Animation zu einem beliebigen Zeitpunkt t in der momentanen Stellung hinzeichnet:
ZeichneBild: ' Kreise wPos! = t! * .5 * Pi! FOR i% = 0 TO 2 w! = wPos! + CSNG(i%) * 2! * Pi! / 3! x% = 160 + CINT(96! * COS(w!)) y% = 100 + CINT(80! * SIN(w!)) CIRCLE (x%, y%), 21, 3 + i%, , , 5! / 6! PAINT (x%, y%), 3 + i% NEXT i% wPos! = t! * -Pi! FOR i% = 0 TO 6 w1! = wPos! + CSNG(i%) * 4! * Pi! / 7! x1% = 160 + CINT(66! * COS(w1!)) y1% = 100 + CINT(55! * SIN(w1!)) w2! = wPos! + CSNG(i% + 1) * 4! * Pi! / 7! x2% = 160 + CINT(66! * COS(w2!)) y2% = 100 + CINT(55! * SIN(w2!)) LINE (x1%, y1%)-(x2%, y2%), 3 NEXT i% FOR i% = 0 TO 6 w1! = wPos! + CSNG(i%) * 4! * Pi! / 7! x1% = 160 + CINT(54! * COS(w1!)) y1% = 100 + CINT(45! * SIN(w1!)) PAINT (x1%, y1%), 5 + i%, 3 NEXT i% RETURN
Wir berechnen also in wPos! die momentane Winkelposition und zeichnen die Planetenkreise sowie den Stern entsprechend.
Wir definieren zuerst die Zeit t0 sowie die Nummer des ersten Bildschirmpuffers:
n% = 0 t0! = TIMER + 10!
Anschliessend können wir unsere Animation abspielen lassen:
WHILE INKEY$ = "" t! = TIMER - t0! PCOPY 4, n% SCREEN , , n%, n% - 1 AND 3 GOSUB ZeichneBild SCREEN , , , n% n% = n% + 1 AND 3 WEND END
Zu Beginn wird die momentane Zeit t berechnet, die
Grundszene in den noch nicht sichtbaren Bildpuffer n%
hineinkopiert. Ausserdem wird die Arbeitsseite auf n% gesetzt, während die Sichtbarseite immer
noch auf n%-1
bleibt. Insgesamt werden 4 Bildschirmseiten
nacheinander verwendet. Der Grund für 4 statt nur 2 Seiten ist derjenige,
dass bei vielen Grafikkarten der effektive Bildschirmseitenwechsel erst beim
sog. VBlank erfolgt, also
verzögert. Da aber die CPU sofort weiterarbeiten kann,
könnte es bei nur 2 Bildschirmseiten vorkommen, dass das Hintergrundbild
bereits wieder gelöscht ist, während es noch gebraucht wird. Mit
3 und mehr Seiten kann dieser Effekt verhindert werden.
Die fertige Animation können Sie hier herunterladen, es misst dabei auch noch die Bildaufbaurate.
Wenn Sie das Programm von vorhin laufen gelassen haben, werden Sie leicht festgestellt haben, dass die Animation trotz einer recht guten Bildaufbaurate (auf meinem Pentium III mit 550 MHz gut 62 Bilder/s) die Animation zwar flimmerfrei erscheint, aber immer noch sehr stark ruckelt. Die Ursache für dieses Problem liefert Ihnen folgendes kleine Programm:
t! = TIMER WHILE INKEY$ = "" t2! = TIMER IF t2! <> t! THEN PRINT t2! - t! t! = TIMER END IF WEND
Ausgabe:
5.078125E-02 5.859375E-02 5.078125E-02 5.859375E-02 5.078125E-02 .0625 .046875
Wie Sie vielleicht aus einem PC-Systemhandbuch wissen, generiert der
Uhrenbaustein genau 216=65'536 Elementarschritte (Ticks) in der
Stunde, was unter anderem auch beim SOUND
-Befehl (siehe Tonprogrammierung) seine Spuren
hinterlassen hat: Sie geben dort die Tondauer auch als Anzahl solcher Ticks an.
Der Mittelwert der obigen Werte entspricht folglich
3600×1000÷216=54,931640625 ms, was dem Mittelwert der
obigen Zahlen entspricht.
Die Systemuhr, welche wir als antreibenden Zeitgeber verwenden möchten, können Sie mit einem Schrittmotor vergleichen, bei welchem sich die Antriebswelle nur ruckartig statt gleichförmig dreht.
Wenn Sie als Mechaniker mit dem vorhin genannten Schrittmotor etwas antreiben wollen, wo Sie eine möglichst gleichförmige Drehbewegung brauchen, helfen Sie sich mit einem Schwungrad (Trägheit, die schnelle Geschwindigkeitswechsel verhindert) und einer elastischen Kupplung (fängt die Stösse bei den Schrittbewegungen ab) ab. Elektroniker kennen dies als sog. Tiefpassfilter, in dem sie mit einem entsprechenden RC-Glied die schnellen Spannungswechsel am Signalausgang verhindern.
Mit genau einem solchen digitalen Tiefpassfilter können Sie das Ruckeln von vorhin vollständig eliminieren.
Ein einfacher digitaler Tiefpassfilter ergibt folgender Algorithmus:
Sie bilden von den letzten n Zeit-Rohwerten einen arithmetischen Mittelwert, wobei Sie jedoch die Werte im Verhältnis n:n-1:n-2...3:2:1 unterschiedlich gewichten, wovon der aktuellste Zeitwert das höchste Gewicht bekommt.
In BASIC formuliert sieht das Ganze etwa so aus:
' Dämpfung: Digitaler Tiefpassfilter INPUT "Dämpfstärke (Anzahl Werte)"; n% DIM tRoh!(1 TO n%) t0! = TIMER + 10! WHILE INKEY$ = "" ' Alles um 1 verschieben FOR i% = 2 TO n% tRoh!(i% - 1) = tRoh!(i%) NEXT i% tRoh!(n%) = TIMER - t0! ' arithmetisches Mittel mit Gewichtung tGed! = 0! FOR i% = 1 TO n% tGed! = tGed! + CSNG(i%) * tRoh!(i%) NEXT i% tGed! = tGed! * 2! / (CSNG(n%) * CSNG(n% + 1)) PRINT USING "Roh: ###.####### Gedämpft: ###.####### Differenz: ###.#####"; tRoh!(n%); tGed!; tGed! - tGedAlt! tGedAlt! = tGed! WEND
Je mehr Werte Sie mitteln, desto stärker werden die Unregelmässigkeiten herausgefiltert, was Sie hinten bei der Differenz sehen können.
Die Gewichtungen und damit den Filter lässt sich grafisch recht hübsch darstellen:
^ | tRoh!(n%) |###### tRoh!(n%-1) |##### tRoh!(n%-2) |#### .. |### tRoh!(2) |## tRoh!(1) |# |
Daraus sollte auch die Entstehung dieser Division mit n*(n+1)/2
ersichtlich sein.
Der Berechnungsalgorithmus in der jetzigen Form kann recht aufwendig werden, weshalb es sich lohnt, diesen zu optimieren. Um das Optimierungspotential zu zeigen, sollten wir einen Vergleich zweier aufeinanderfolgenden Werte machen:
^ | tRoh!(n%+1) |++++++ tRoh!(n%) |#####- tRoh!(n%-1) |####- tRoh!(n%-2) |###- .. |##- tRoh!(2) |#- tRoh!(1) |- | ## = bleibt unverändert ++ = wird dazuaddiert -- = wird subtrahiert
Den vorherigen tGed!-Wert kennen wir ja noch, also
genügt es, zum letzen Wert die neue Zeit n dazuzählen und dafür die einfache
Summe aller Zeiten wegzuzählen.
Dabei die Division n*(n+1)/2
nicht vergessen:
DivK! = 2! / (CSNG(n%) * CSNG(n% + 1)) tRohNeu! = TIMER - t0! tGed! = tGed! + (tRohNeu! * CSNG(n%) - ts!) * DivK!
Auch die Divisionskonstante kann einmalig berechnet werden.
Als weitere Optimierung kann noch die Verschiebung aller Rohzeitwerte in der Feldvariable tRoh!() eliminiert werden, denn ts! können Sie ebenfalls mit der Formulierung
ts! = ts! + tRohNeu! - tRoh!(h%) tRoh!(h%) = tRohNeu! h% = h% MOD n% + 1
beseitigen, da Sie tRoh!() als reinen FIFO-Stack verwenden können. Damit sieht die neue Version des vorherigen Programms etwa so aus:
' Dämpfung: Digitaler Tiefpassfilter optimiert INPUT "Dämpfstärke (Anzahl Werte)"; n% DIM tRoh!(1 TO n%) t0! = TIMER + 10! ts! = 0! FOR i% = 1 TO n% tRoh!(i%) = TIMER - t0! ts! = ts! + tRoh!(i%) NEXT i% tGed! = TIMER - t0! h% = 1 DivK! = 2! / (CSNG(n%) * CSNG(n% + 1)) WHILE INKEY$ = "" tRohNeu! = TIMER - t0! tGed! = tGed! + (tRohNeu! * CSNG(n%) - ts!) * DivK! ts! = ts! + tRohNeu! - tRoh!(h%) tRoh!(h%) = tRohNeu! h% = h% MOD n% + 1 PRINT USING "Roh: ###.####### Gedämpft: ###.####### Differenz: ###.#####"; tRoh!(n%); tGed!; tGed! - tGedAlt! tGedAlt! = tGed! WEND
Genau diesen Filter müssen Sie noch in das Hauptprogramm unserer Beispielanimation einbauen:
t0! = TIMER + 10! DIM tRoh!(63) ts! = 0! FOR i% = 0 TO 63 tRoh!(i%) = TIMER - t0! ts! = ts! + tRoh!(i%) NEXT i% t! = TIMER - t0! h% = 0 WHILE INKEY$ = "" tRohNeu! = TIMER - t0! t! = t! + (64! * tRohNeu! - ts!) / 2080! ts! = ts! - tRoh!(h%) + tRohNeu! tRoh!(h%) = tRohNeu! h% = h% + 1 AND 63 PCOPY 4, n% SCREEN , , n%, n% - 1 AND 3 GOSUB ZeichneBild SCREEN , , , n% n% = n% + 1 AND 3 WEND
Sie finden die fertige Version hier zum Herunterladen. Damit ist unsere Animation wirklich perfekt, wie Sie hoffentlich auf Ihrem PC-Bildschirm selber erleben können :-).
Mit dem Parameter n haben Sie vorhin gesehen, dass Sie die Stärke der Dämpfung beeinflussen können. Diesen Wert sollten Sie in der Praxis weder zu gering noch zu gross wählen. Es hängt vor allem davon ab, wie gleichmässig der Rechenzeitverbrauch eines Bildaufbauschrittes ist. Das hier gezeigte Beispiel ist sehr regelmässig, sodass ein grosses n möglich ist, denn es wird ja immer die genau gleiche Menge an Rechenoperationen und BASIC-Zeichenbefehle aufgerufen. Bei der Billard-Simulation dagegen bedeutet die Impulsauswertung eines stattfindenden Stosses zwischen zwei Kugeln oder mit der Bande vor allem auf älteren Rechnern einen kurzfristigen Anstieg der Rechenzeit. Aus diesem Grund ist dort n sehr massvoll gewählt.
Sie können dies sehr leicht selber nachvollziehen, in dem Sie das Programm mit Pause kurz bremsen und mit Tastendruck wieder weiter laufen lassen: Bei sehr grossem n dauert es dann sichtlich eine Weile, bis sich die Animation wieder im Taktrhythmus befindet.