Spiele Programmierung
1. Sprites -
rasante Bewegungen auf dem Bildschirm
In einem Rollenspiel als Fantasy-Spielfigur, die über einer Landschaft längst vergangener Zeiten wandert oder als Raumgleiter in einem Aktion-Game : überall werden Sprites benutzt. Im Prinzip kann man so ziemlich alles als Sprite bezeichnen, das sich in irgendeiner Form über einen Hintergrund bewegt.
Nun stellt sich aber die Frage, wie man solche Sprites auf den Bildschirm bringt.
Grundlagen
Das Grundprinzip, um mit Sprites zu arbeiten ist naheliegend: Man kopiert zuerst den Hintergrund auf den Bildschirm und darüber werden dann die Sprites als eine Art Bitmap dargestellt.
Das klingt sehr einfach - ist es aber nicht.
Zuerst kann man sich fragen, ob es sinnvoll ist, immer den gesamten Hintergrund neu darzustellen.
Bevor die Sprites auf ihren neuen Positionen dargestellt werden können, müssen sie zuerst vom Bildschirm gelöscht werden. Nun könnte man immer nur den Bereich des Hintergrundes sichern, der vom Sprite überschrieben wird und diesen dann beim nächsten Bildschirmaufbau über das alte Sprite kopieren und darüber wird dann das neue Sprite dargestellt. Es ist aber meistens schneller, wenn man immer den gesamten Hintergrund darstellt, da bei mehreren Sprites die Verwaltung schwierig und zeitraubend wird.
Als nächstes tritt das Problem auf, daß man Sprites nur als Rechtecke behandeln kann, da alles andere einen zu hohen Rechenaufwand benötigen würde. Nun ist es beinahe unmöglich oder zumindest nicht sehr schön, wenn man alle Sprites als primitive Rechtecke ohne transparente Stellen darstellt. Man könnte so nicht einmal eine einfache Kugel oder ein Dreieck über den Hintergrund darstellen. Es muß also einen Weg gefunden werden, um Bereiche in einem Sprite zu definieren, die transparent (bzw. nicht) dargestellt werden sollen.
Man könnte für jedes Pixel einen Transparenz–Grad definieren oder die durchsichtigen Stellen durch Bitverknüpfungen erreichen. Der einfachste Weg ist aber wohl, daß man eine Farbe als transparent definiert. Beim Darstellen des Sprites werden einfach nur die Pixel gesetzt, die nicht diese Farbe besitzen.
2. Das 2 - Seiten – Prinzip oder
Double Buffering
Der Bildschirm baut standardmäßig bei einer Auflösung von 320*200 ein Bild 70 mal in einer Sekunde neu auf.
Jetzt müßte man zwischen zwei dieser Neudarstellungen den Hintergrund und alle Sprites neu bearbeiten. Ansonsten wäre der Zeitraum, in dem der Hintergrund neu dargestellt wird, und alle Sprites überschrieben werden, für den Spieler sichtbar. Dadurch wäre manchmal nur der Hintergrund auf dem Bildschirm zu sehen, und die Sprites würden flimmern und sich mit dem Hintergrund vermischen.
Bei aufwendigeren Spielen ist dieser Zeitraum einfach zu kurz, um die gesamte Bearbeitung des Hintergrundes und der Sprites darin unterzubringen. Darum bedient man sich einer zweiten Bildschirmseite: Eine fertige Bildschirm - Seite wird angezeigt während auf der zweiten Seite das nächste Bild bearbeitet wird; ist die Bearbeitung zu Ende, wird die neue, fertige Seite angezeigt, während auf der anderen wieder das nächste Bild fertiggestellt wird. So hat man immer nur eine fertige Seite auf dem Bildschirm, und ein flimmern wird verhindert.
3. Jetzt wird es ernst : Der Mode X
Eine standardmäßige VGA-Graphikkarte hat mindestens 265K Speicher. Bei einer Auflösung von 320*200 benötigt man 64000 Bytes, um eine gesamte Bildschirmseite zu speichern. Somit kann man theoretisch 4 Bildschirmseiten im VGA-RAM anlegen. Allerdings muß man erst einen Weg finden, um auf die gesamten 256k zuzugreifen zu können, da man über das A000h-Segment (dort wird der VGA-RAM eingeblendet) im Hauptspeicher eigentlich nur auf 64k zugreifen kann. Somit könnte man nur ein Viertel des VGA-RAMs bzw. eine Bildschirmseite verwenden, was es uns unmöglich machen würde, daß Double-Buffering-Prinzip zu verwenden.
Die Lösung bietet hier der sogenannte Mode X. Das Grundprinzip des Mode X liegt darin, das der VGA-RAM auf 4“-Bit-Planes” aufgeteilt wird, die einzeln selektiert und somit in das A000h-Segment eingeblendet werden können.
Wie der Speicher auf die einzelnen Planes aufgeteilt wird, zeigt die folgende Abbildung:
Nun sind nur noch 16 kByte pro Plane für eine Bildschirmseite belegt. Somit kann man noch 3 weiter Bildschirmseiten verwalten, von denen jeweils ein Viertel in die einzelnen Planes eingeblendet wird.
Um eine Plane zu selektieren, muß man über das Indexregister 3C4h auf das Timing-Sequenzer-Register 2 die gewünschte Plane-Nummer schreiben. Dies kann mit einem Word–Zugriff mit einem Befehl erledigt werden.
Timing - Sequenzer (TS) – Register 2 |
|
Bit |
Bedeutung |
7-4 |
Reserviert |
3 |
Schreibzugriff auf Plane 3 |
2 |
Schreibzugriff auf Plane 2 |
1 |
Schreibzugriff auf Plane 1 |
0 |
Schreibzugriff auf Plane 0 |
Beispiel, um Plane 0 zu selektieren:
mov dx,03C4h ;TS - Indexregister
mov al,02h ;Register 2
mov ah,01 ;Plane 0
out dx,ax ;selektieren
Bevor man aber die einzelnen Planes selektieren kann muß man den Mode X erst einmal initialisieren. Der Mode X basiert auf dem BIOS–Graphikmodus 13h. Allerdings sind zusätzlich einige Manipulationen von VGA–Registern nötig, dessen Erklärung sehr tief in die Thematik eingehen würde und darum den Rahmen dieses Artikels sprengen würde. Darum möchte ich hier einfach nur die Funktion presentieren:
void initModeX(void)
{
asm{
mov ax,0x0013 //Mode 13h setzen
int 0x10
mov dx,0x3c4 //Timing Sequenzer
mov al,4 //Register 4 (Memory Mode):
out dx,al //Bit 3 löschen - Chain4 aus
inc dx
in al,dx
and al,0x0f7
or al,0x4 //Bit 2 setzen - Odd/Even Mode aus
out dx,al
dec dx
mov ax,0x0f02 //Register 2 (Write Plane Mask):
out dx,ax //0fh: alle Planes beim Schreiben ein
mov ax,0x0a000 //Bildschirmspeicher löschen
mov es,ax
xor di,di
xor ax,ax
mov cx,0x0ffff
cld
rep stosw
mov dx,0x3d4 //CRTC
mov al,0x14 //Register 14h (Underline Row Adress):
out dx,al
inc dx
in al,dx //Bit 6 löschen - Doubleword adress. aus
and al,0x0bf
out dx,al
dec dx
mov al,0x17 //Register 17h (CRTC Mode):
out dx,al //Bit 6 setzen - Byte Mode ein
inc dx
in al,dx
or al,0x40
out dx,al
mov dx,0x03ce //Write Mode 0 setzen
mov ax,0x4005 //Über GDC Register 5 (GDC Mode)
out dx,ax
}
}
Jetzt benötigen wir noch einen Weg, um zwischen zwei oder mehreren Seiten umzuschalten. Das kann man mit den “Linear Starting Address” - Registern durchführen. Mit diesen Registern kann man den Offset festlegenden, bei der die VGA - Karte mit dem auslesen der Bilddaten beginnt. Der Offset errechnet sich mit der Formel off=320 * 200 / 4 * Seitennummer.
Um auf die Register des CRTC (Cathod Ray Tube Controller) zuzugreifen, muß man über das Indexregister 3D4h das gewünschte Register selektieren, um dann das jeweilige CRTC - Register über das Datenregister 3D5h zu bearbeiten. Dies ist wiederum mit einem Wort - Zugriff in einem realisierbar.
CRTC - Register 0Ch: Linear Starting Address High |
|
Bit |
Bedeutung |
7-0 |
Bit 15-8 der 16bittigen Startadresse |
CRTC – Register 0Dh: Linear Starting Address Low |
|
Bit |
Bedeutung |
7-0 |
Bit 7-0 der Startadresse |
Beispiel um eine neue Startadresse zu setzen. Der benötigte Offset wird aus der Startspalte (x) und der Startzeile (y) berechnent.
void setViewStart(SWORD x,SWORD y)
{
asm{
mov ax,y
mov bl,80 //80*4 = 320 Pixel
mul bl
add ax,x //Offset
mov cl,al
mov dx,0x3d4 //CRTC
mov al,0x0c //Register 0ch(Linear Starting Adress High)
out dx,ax //Bits 15:8 setzten
mov al,0xd //Register 0dh(LSA Low)
mov ah,cl //Bits 7:0 setzen
out dx,ax
}
}
Und eine Routine, die uns zwischen den ersten zwei Seiten umschaltet:
UWORD aktiveViewPage=1; //Angezeigte Seite
UWORD aktivePage=0xa000; //bearbeitbare Seite
void switchX(void)
{
if (aktiveViewPage==0)
{
aktiveViewPage=1;
setViewStart(0,200); //zweite Seite anzeigen
aktivePage=0xa000; //erste Seite bearbeiten
}
else
{
aktiveViewPage=0;
setViewStart(0,0); //erste Seite anzeigen
aktivePage=0xa000+0x03E8; //zweite Seite bearbeiten
}
}
Die Variable aktiveViewPage dient uns als Erkennungsmittel, welche Seite gerade angezeigt wird. Der Offset ins VGA – RAM der Seite wird gleich mit dem VGA – Segment kombiniert und in aktivePage gespeichert.
Als nächstes eine kleine Routine die einen Pixel mit der Farbe col an die Position x,y setzt.
void putPixel(SWORD x, SWORD y, BYTE col)
{
asm{
mov ax, aktivePage //Segment laden
mov es, ax
mov cx, x //Plane errechnen
and cx, 3
mov ax, 1 //umformen
shl ax, cl
mov ah, al
mov dx, 0x3c4
mov al, 2
out dx, ax //und setzten
mov ax, 80 //Offset errechnen
mul y
mov di, ax
mov ax, x
shr ax, 2
add di, ax
mov al, byte ptr col
mov es:[di], al //Farbe setzten
}
}
3. Das Finale: ein kleines Spiel
Nun wollen wir ein kleines “Spiel” zusammenstellen, daß ein Sprite als unseren Helden über einem Hintergrundbild hin und her pendeln läßt.
Zunächst benötigen wir einen Weg, um unseren Helden und seinem Hintergrund graphisch in unser Spiel zu bekommen. Dazu werden wir die Funktion aus dem Artikel “Das PCX Format verwenden. Die Daten werden wir mit globalen Pointern und die Dimensionen mit jeweils zwei integer – Variablen sichern.
Der Grundkopf mit einigen zusätzlichen Funktionen, die wir verwenden, könnte so aussehen:
#include <conio.h>
#include <dos.h>
#include <stdlib.h>
#include <io.h>
#include <fcntl.h>
#include <string.h>
#include <sys\stat.h>
#include <alloc.h>
typedef signed long int SLONG; //32 bit signed.
typedef unsigned long int ULONG; //32 bit unsigned.
typedef signed int SWORD; //16 bit signed.
typedef unsigned int UWORD; //16 bit unsigned.
typedef signed char BYTE; //8 bit signed.
typedef unsigned char UBYTE; //8 bit unsigned.
#define TRUE 1
#define FALSE 0
/********************************************************************/
/* Pcx Header Struktur */
/********************************************************************/
typedef struct pcx_header
{
...
}pcx_header;
#define PCX_BYTEMODE 0 //nächstes Byte bearbeiten
#define PCX_RUNMODE 1 //Wiederholschleife
#define PCX_BUFLEN 4*1024 //Bufferlänge beim Einlesen
UWORD aktiveViewPage=1; //angezeigte Seite
UWORD aktivePage=0xa000; //bearbeitbare Seite
UBYTE * background=NULL; //Pointer auf Hintergrund Daten
UBYTE * sprite=NULL; //Pointer auf Sprite Daten
UWORD backx=0,backy=0; //Breite / Höhe vom Hintergrund
UWORD spritex=0,spritey=0; //und vom Sprite
UBYTE palette[256*3]; //Palette des Hintergrunds/Sprites
void initModeX(void);
void setViewStart(SWORD x,SWORD y);
void switchX(void);
void putPixel(SWORD x, SWORD y, BYTE col);
UBYTE * loadpcx
(BYTE *filename, struct pcx_header * ph,UBYTE * palette);
void WaitRetrace(void);
void setpal(unsigned char * palette);
void getpal(unsigned char * palette);
void fadepalin(int from,int count,UBYTE *pal);
void fadepalout(int from,int count);
...
void closemodex(void) //Schaltet zurück in den Textmodus
{
asm{
mov ax,0x3 //Biosinterrupt, Textmodus
int 0x10
}
}
Jetzt benötigen wir noch zwei Funktionen, die uns unsere Daten auf den Bildschirm bringen. Beim Mode X gibt es da einige Möglichkeiten, ich möchte hier aber nur den wahrscheinlich einfachsten Weg über die Funktion putPixel wählen. Für ein umfangreiches Spiel ist diese Lösung aber nicht verwendbar, da sie viel zu langsam ist.
Eine sehr einfache Möglichkeit um ein Image mit der Höhe höhe und der Breite breite ab der Position x,y darzustellen:
void viewImg(UWORD x, UWORD y, UWORD breite, UWORD hoehe,UBYTE *data)
{
UWORD i,j;
for(i=y;i<hoehe+y;i++)
{
for(j=x;j<breite+x;j++,data++)putPixel(j,i,*data);
}
}
Und das ganze noch einmal für ein Sprite. Als transparente Farbe wurde 0 gewählt.
void viewSprite
(UWORD x, UWORD y, UWORD breite, UWORD hoehe,UBYTE *data)
{
UWORD i,j;
for(i=y;i<hoehe+y;i++)
{
for(j=x;j<breite+x;j++,data++)if(*data!=0)putPixel(j,i,*data);
}
}
Jetzt fehlt uns nur noch die Hauptfunktion.
Sie muß einerseits unsere Daten laden, und andererseits in einer Schleife unseren Helden hin und herlaufen lassen.
Bei dem Hintergrundbild und dem Sprite ist darauf zu achten, daß diese die selbe Palette haben, da es sonst zu Farbverfälschungen kommt.
SWORD main()
{
struct pcx_header head; //PCX header
SWORD x=0,inc=1; //X Position / Richtung des Sprites
UBYTE tmpPalette[768]; //Temporär - Palette, um den Bilschirm
//auf schwarz zu setzten
printf("Loading ...");
//Hintergrund laden
background=loadpcx("back.pcx",&head,palette);
if(background==NULL) //Nicht erfolgreich?
{//Fehler!!!
printf("Fehler beim Laden des Hintergrunds");
return -1;
}
backx=head.xmax-head.xmin+1; // Alles OK à Dimensionen berechnen
backy=head.ymax-head.ymin+1;
//Sprite laden
sprite=loadpcx("sprite.pcx",&head,palette);
if(sprite==NULL) //Nicht erfolgreich?
{//Fehler
free(background); //Hintergrund freigeben
printf("Fehler beim Laden des Sprites");
return -1;
}
spritex=head.xmax-head.xmin+1; //Alles OK à Dimensionen berrechnen
spritey=head.ymax-head.ymin+1;
fadepalout(0,256); //Vom Textmodus ausfaden
initModeX(); //Mode X aktivieren
memset(tmpPalette,0,256*3); //Palette auf 0 (schwarz) setzen
setpal(tmpPalette);
//Bild auf nicht sichtbarer Seite anzeigen
viewImg(0,0,backx,backy,background);
switchX(); //Seiten umschalten(noch immer
//unsichtbar, weil Palette schwarz!)
fadepalin(0,256,palette); //Palette einfaden
while(!kbhit()) //Hauptschleife! Bis Taste
{ //Daten anzeigen
viewImg(0,0,backx,backy,background);
viewSprite(x,70,spritex,spritey,sprite);
switchX(); //Seiten umschalten
if(inc>0) //Sprite nach rechts bewegen
{
if(x<320-inc-spritex)x+=inc; //Geht's noch?
else inc=-inc; //Nein! Andere Richtung
}
else //Sprite nach links bewegen
{
if(x+inc>=0)x+=inc; //Geht's noch?
else inc=-inc; //nein à Andere Richtung
}
}
fadepalout(0,256); //Palette Ausfaden
closemodex(); //In den Textmodus zurück
free(background); //Daten freigeben
free(sprite);
return 0;
}
Ich hoffe, daß ich einigen geholfen habe und wünsche viel Spaß beim Programmieren.