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.

spiel1.JPG

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.

spiel2.JPG

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.