DOS Programmierung Tutorial – Bewegung
In diesem Tutorial zeige ich euch wie man Bewegliche Objekte auf den Bildschirm zeichnet und mit der Tastatur steuert. Im letzten Teil haben wir ja bereits den Hintergrund der Szene gezeichnet, jetzt kommt darauf ein bewegliches Objekt.
DOS Programmierung Tutorial – Bewegung
Jede interaktive Anwendung besteht aus zumindest einem beweglichen Objekt. Diese Dynamik hat in den frühen 80er Jahren aufsehn erregt, da die Rechner plötzlich leistungsfähig genug waren um flüssige Bewegung darzustellen. So hat bei der Vorstellung des Amiga 500 die berühmte Bouncing Ball Demo für Aufsehen erregt. Da wir unter DOS den Videospeicher von VGA direkt manipulieren können ist jede Änderung sofort mit dem nächsten Zeichenvorgang am Bildschirm sichtbar. Das können wir ausnutzen und so auch Objekte hineinzeichnen, die sich verändern können.
Quadrat zeichnen
Das einfachste zu zeichnende Objekt ist ein Quadrat. Um ein solches zu zeichnen definiert man eine neue Speicherstruktur:
1 2 3 4 5 6 | typedef struct { byte color; int x, y; int width; byte *buffer; } square; |
Und legt eine Instanz davon in der Hauptschleife mit Werten an:
1 2 3 4 5 6 7 | square square1; square1.width = 50; square1.x = 5; square1.y = 5; square1.color = 255; square1.buffer = (byte*)malloc(square1.width * square1.width); |
x und y ist die Position an welcher Stelle das Quadrat gezeichnet werden soll. width ist dessen Dimension, also die Größe und color ein Index der Farbpalette in welcher dieses gezeichnet werden soll. Interessant wird es beim Buffer. Das Programm zeichnet einmalig den Hintergrund und danach jedesmal die Objekte. Bewegt man das Objekt, dann muss man die Farben des Hintergrunds die wieder zum Vorschein kommen wiederherstellen, ansonsten hat man einen Radierer programmiert. In dem Buffer speichern wir den Hintergrund der Fläche vom Quadrat. Die Farbe jedes Pixel vor dem Zeichnen des Quadrats. Vor jedem Zeichenvorgang wird dieser Inhalt wiederhergestellt. Für diesen Vorgang wurden die Funktionen:
1 2 3 | store_square(&square1); restore_square(&square1); draw_square(&square1); |
implementiert. Das Zeichnen ist relativ einfach:
1 2 3 4 5 6 7 8 9 10 11 12 13 | /* draws a square object with given color and dimensions into VGA buffer */ void draw_square(square *s) { int i, j; for(j=s->y; j < s->y + s->width; ++j) { for(i=s->x; i < s->x + s->width; ++i) { byte far *dest = VGA + i + j * SCREEN_WIDTH; *dest = s->color; } } } |
Man kann direkt in den Videospeicher (ab Adresse VGA) schreiben. Beim Quadrat werden pro Zeile von x bis x + width alle Pixel auf die Farbe color gesetzt. Das von Zeile y bis y + width. Komplizierter ist das Kopieren der Farbinformation vom Hintergrund in den buffer beziehungsweise das zurücksetzen des buffers in den Videospeicher. Der Vorgang ist ein byte-weises Kopieren von Information, die Schwierigkeit besteht darin auch die korrekten Daten zu schreiben. Ein Fehler erkennt man in der Ausgabe am Bildschirm sofort durch Artefakte und Fehler unterschiedlichster Art.
Bewegung
Das Quadrat kann mit den Pfeiltasten über den Bildschirm bewegt werden, es kann nicht aus dem Bildschirm fallen, es gibt also eine Kollisionsprüfung mit der Bildschirmgrenze. Implementiert wurde das in der switch Abfrage in der wir schon im Keyboard Artikel die Tasten abgefragt haben. Die Prüfung ist eigentlich recht simpel. Je nach Richtung wird x oder y erhöht oder vermindert und auf die Grenzen des Bildschirms geprüft. Wichtig ist, dass man die Größe des Quadrats auch berücksichtigt:
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 | switch(kc) { case 0x48: /* up */ square1.y -= 2; if(square1.y < 0) { square1.y = 0; } break; case 0x4b: /* left */ square1.x -= 2; if(square1.x < 0) { square1.x = 0; } break; case 0x4d: /* right */ square1.x += 2; if(square1.x + square1.width > SCREEN_WIDTH) { square1.x = SCREEN_WIDTH - square1.width; } break; case 0x50: /* down */ square1.y += 2; if(square1.y + square1.width > SCREEN_HEIGHT) { square1.y = SCREEN_HEIGHT - square1.width; } break; } |
Flackern entfernen
Das Bild wird unter DOS auf einem CRT Monitor ausgegeben. Das bedeutet, das Bild wird mit einer Elektronenkanone zeilenweise ca. 60 Mal (bei einem 60 Hz Monitor) pro Sekunde aufgebaut. Ändert man während einem solchen Zeichenvorgang den Speicher ergeben sich Zwischenbilder (ein Teil vom alten Status ist mit einem Teil des neuen Status gemischt). Dieser Effekt resultiert in einem wahrgenommenen Flackern von Bereichen im Bild. Die Lösung war für Entwickler darauf zu warten, bis ein Bild vollständig gezeichnet war und in der kurzen Zeitspanne zum nächsten Zeichenvorgang den Speicher zu schreiben.
1 2 | #define INPUT_STATUS 0x3DA /* CRT controller 6845 status register */ #define VRETRACE_BIT 0x08 /* retrace scanline bit */ |
Für diese Abfrage gibt es wie immer ein Hardware Register. Unter der Adresse 0x3DA hat man Zugriff auf den CRT Controller. Das Bit Nummer 8 (0x08) liefert als Ergebnis den Zustand des vertikalen Retraces. Dabei wird nach dem Zeichnen des Bildes der Elektronenstrahl wieder auf die erste Position links oben im Bild zurückgesetzt. In der kurzen Zeitspanne schreiben wir. Implementiert wird das in einer Funktion in der wir einfach warten, bis das Bit gesetzt ist:
1 2 3 4 5 6 | /* wait till ram access is allowed to fix flickering */ void wait_for_retrace() { while (inp(INPUT_STATUS) & VRETRACE_BIT); while (!(inp(INPUT_STATUS) & VRETRACE_BIT)); } |
Fazit
In diesem Beispiel haben wir ein einfaches Objekt über den Hintergrund gezeichnet. Durch die Tastatur lässt sich dieses sogar bewegen. Ich habe gezeigt, dass man unter DOS viel Arbeit hat mit Dingen die heute Bibliotheken kapseln. Wir mussten Speicherbereiche kopieren und zwischenspeichern und ein flackern im Bild, dass durch die Art wie gezeichnet wird durch eine Abfrage in einem Hardware Register lösen.
Hi, interessante Reihe.. kannst du nochmal den kompletten Sourcecode veröffentlichen?
Hallo, den gesamten Code findet man immer aktuell auf meinem GitHub Account: https://github.com/Ziagl/DOS-C-PLUS-PLUS