AnMaBaGiMa's Home

DirectDraw - Tetris-Clone

Nachdem die Spielsteuerung des Tetris-Spieles fertig ist und die vollen Zeilen verschwinden, wenn sie sollen, fehlt also nur noch der Punktestand und ein kleines i-Tüpfelchen, welches dieses Tetris zu etwas besonderem macht.

Die Zählung der Punkte sollte jeder für sich fest legen. Wir haben uns hier dafür entschieden, für jeden Spielstein der gesetzt wurde - also "fest" ist - gibt es einen Punkt und für jede Zeile die verschwindet, gibt es 50 Punkte extra. Diese Punkte speichern wir in der Variablen PunkteStand.
Um nun den Schwierigkeitsgrad des Spieles langsam zu erhöhen wird die Geschwindigkeit mit der die Spielsteine herunter purzeln alle 100 Punkte leicht erhöht.
Die Spielgeschwindigkeit erhöht sich, wenn man das Zeitintervall des Timers verringert. Also legen wir das Startintervall in einer Konstanten und das aktuelle Intervall in einer Variablen ab.

#define STARTSPEED 250 //Startgeschwindigkeit mit der die Steine runterkommen //je kleiner die Zahl desto schneller fallen sie!! int GameSpeed; //aktuelle Spielgeschwindigkeit int PunkteStand = 0; //Punktestand
Um die Geschwindigkeit während des Spieles ändern zu können, muss der Timer angehalten und mit dem neuen Intervall wieder gestartet werden. Wir werden die Spielgeschwindigkeit je 100 Punkte um 10 erhöhen. Das Intervall wird aber auf ein Minimum von 50 Millisekunden begrenzt - das ist schon ganz schön schnell. Das heisst, dass die höchste Spielgeschwindigkeit nach 2500 Punkten erreicht wird. Überprüft werden die Punkte bei jedem Neuzeichnen des Fensters.

[...] case WM_PAINT: BeginPaint(...); [...] EndPaint(...); if (PunkteStand > 0) { Div = (int)(PunkteStand / 100); // Div ändert sich nur alle 100 Punkte KillTimer(hwnd, ID_TETRISTIMER);//Timer stoppen GameSpeed = STARTSPEED - 10*Div;//Spielgeschwindigkeit berechnen if (GameSpeed < 50) GameSpeed = 50; //Intervall auf min. 50 Millisekunden begrenzen SetTimer(hwnd, ID_TETRISTIMER, GameSpeed, TetrisMove);//Timer setzen } break;
Der nächste Schritt soll sich nun damit beschäftigen wie wir die erzielten Punkte auch während des Spiels anzeigen können. Zu diesem Zweck ist es empfehlenswert einen für die Ausgabe bestimmten Bereich im Hintergrundbild - ähnlich dem Spielfeld - herrauszuarbeiten, was
im folgenden Bild bereits geschehen ist.

Tetrisbild mit Punktefeld

Es ist wichtig zu wissen an welcher Stelle sich das neue Rechteck im Bild befindet. Dieses hier ist bei (250, 220) und 120 Punkte breit und 60 Punkte hoch.
Zur Darstellung der Punkte werden wir die Funktion DrawPunkteStand codieren. Diese wird wie die Funktion DrawPlayfield als erstes das Rechteck schwärzen mittels der GDI-Funktion FillRect und anschliessend mit der GDI-Funktion DrawText der Punktestand in diesem Bereich ausgegeben. Lediglich angedeutet sind noch eine Zeile Bonus und eine Zeile Highscore die lediglich der Demonstartion von mehrzeiliger Textausgabe dienen sollen.

void DrawPunktestand(void) { HDC hdc; RECT FRect; char *lpPunkteText; lpPunkteText=(char *)malloc(255); //Speicher fü Textpuffer allockieren //formatierten Text in den Puffer schreiben sprintf(lpPunkteText, "Punkte : %u\n" "Bonus : 0\n" "Highscore: 0\n", PunkteStand); //Das Rechteck innerhalb der lpDPaint7 - Zeichenfläche FRect.top = 220; FRect.left = 250; FRect.bottom = 280; FRect.right = 370; //Gerätekontext der Zeichenfläche hohlen für GDI-Funktionen if (lpDPain7t->lpVtbl->GetDC(lpDPaint7, &hdc)== DD_OK) { //Rechteck leeren FillRect(hdc, &FRect, (HBRUSH)GetStockObject(BLACK_BRUSH)); //Textfarbe setzen SetTextColor(hdc, RGB(255, 100, 0)); //Hintergund des Textes Transparent SetBkMode(hdc, TRANSPARENT); //Textausgabe DrawText(hdc, lpPunkteText, strlen(lpPunkteText), &FRect, DT_LEFT | DT_TOP); //Freigabe des Gerätekontext lpDPaint7->lpVtbl->ReleaseDC(lpDPaint7, hdc); } free(lpPunkteText);//Text-Speicher wieder freigeben }
Wenn wir nun so unsere ziemlich komplette Tetrisversion spielen, werden wir feststellen, dass die Spielsteine im Verhältnis zum Spielfeld ganz schön groß geraten sind. Mit ganz wenig Änderungen werden wir die Spielsteingrösse nun hlbieren.
Das bedeutet als erstes, dass das Bild des Spielsteins von 32x32 auf 16x16 gebracht werden muss.
Als nächstes vergrössern sich natürlich die Spalten- und Zeilenzahlen des Spielfeldrasters:

#define TBREITE 12 //Spielfeldbreite vorher 5 + 2 Rand, jetzt 10 + 2 Rand #define THOEHE 18 //Spielfeldhöhe vorher 8 + 2 Rand, jetzt 16 + 2 Rand
Die letzte Änderung wird in der Funktion DrawStone vorgenommen. Hier hatten wir die Spielsteinposition im Ratser mit 32 multipliziert um auf die Zeichenflächenposition zu kommen. Diese werden nun durch 16 ersetzt.

void DrawStone(byte x, byte y) { int sx, sy; RECT SRect; sx = x * 16; sy = y * 16; SRect.top = 0; SRect.left = 0; SRect.right = 16; SRect.bottom = 16; lpDGame7->lpVtbl->BltFast(lpDGame7, sx, sy, lpDStein7, &SRect, DDBLTFAST_WAIT); }
Das wars auch schon. Wenn Sie das Projekt nun erstellen , werden sie feststellen, dass es sich mit dieser Spielsteingrösse um einiges besser Spielen lässt. Doch dem Ganzen fehlt irgendwie noch der letzte Schliff. Wenn eine Zeile gefüllt ist, wird diese entfernt und alle darüberliegenden Spielsteine rutschen nach. Dabei lösen sich die Spielsteine der Zeile irgendwie in Luft auf und sind von einem zum anderen Moment einfach weg. Das ist natürlich völlig unspektakulär - oder ?
Wie wäre es, wenn jeder Stein in einer kleinen Explosion oder Ähnliches das zeitliche segnet ? Das wäre doch eine tolle Abwechslung.
Eine solche Explosion stellt man am einfachsten durch mehrere Einzelbilder dar die nacheinander an der selben Stelle dargestellt werden. Zu diesem Zweck erstellen wir ein Bild, welches neben dem normalen Spielstein die Explosionssequenzen enthält:

Spielstein mit Sequenz

Jedes Einzelbild ist 16x16 Bildpunkte gross. Wir könnten also ein 16x16 grosses Rechteck über die Bilder "stülpen", dann kann der linke Rand als Vielfaches von 16 ermittelt werden. Das macht hier 10 Einzelbilder. Nun Überlegen wir uns eine möglicht einfache Art diese Animation darzustellen. In der Funktion CheckBombStone wird die Zeile ermittelt in der die Steine aufgelöst werden können. An dieser Stelle "klinken" wir uns sozusagen ein. Hier werden in einer Schleife alle Spielsteine im Spielfeldraster die entfernt werden können mit den Werten von 1 bis 10 belegt und das Spielfeld neugezeichnet. Die Routine DrawPlayfield ruft nun in jedem Falle die Routine DrawStone wenn das Feld im Spielfeldraste nicht Null ist. Wenn wir nun der Funktion DrawStone den Wert des Spielsteines im Spielfeldraster mitteilen, kann man aus dem Spielsteinbild, welches auch die Sequenzen enthält, genau das Rechteck herraus greifen und in das Spielfeld zeichnen, das dieser Zahl entspricht.
Da die festen Spielsteine mit 1 und die bewegten mit 2 im Ratser gekennzeichnet werden, sind die ersten beiden Spielsteinbilder innerhalb des gesammten gleich. Erst ab dem 3. Bildchen beginnt die Explosionssequenz.
Für die Funktion DrawPlayfield ergibt sich daraus folgene Änderung:

void DrawPlayfield(void) { [...] for (y=1;y<THOEHE-1;y++) { for (x=1;x<TBREITE-1;x++) { //wenn im Tetris-Raster ein Feld != 0 dann ist da ein Spielstein if (Tetris[y][x] != 0) DrawStone((byte)(x-1), (byte)(y-1), Tetris[y][x]); } } }
Da DrawStone nun also mit einer Art Index auf das entsprechende Bildchen aufgerufen wird, muss diese Funktion nun das richtige Bild für den Spielstein in das Spielfeld zeichnen.

void DrawStone(byte x, byte y, byte picadr) { int sx, sy; RECT SRect; sx = x * 16; sy = y * 16; SRect.top = 0; //oben //picadr beginnt bei 1, der linke Rand des ertsen Bildes bei 0 SRect.left = (picadr-1)*16;//linker Rand des Quellrechteckes SRect.right = (picadr)*16;//rechter Rand -> 16 + linker Rand SRect.bottom = 16;//unten lpDGame7->lpVtbl->BltFast(lpDGame7, sx, sy, lpDStein7, &SRect, DDBLTFAST_WAIT); }
Ein wenig kniffelig wird nun noch mal die Steuerung dieser Animation , wenn eine Zeile aufgelöst wird. Ausgeführt wird dies in der Funktion CheckBombStone. Während dieser Animation ist es notwendig den Timer zu deaktivieren - da dieser sonst bereits den nächsten Stein von oben herein purzeln lassen würde. Nach der Animation wird der Timer wieder aktiviert. Dies mach erforderlich, dass der Funktion das Fensterhandle der Anwendung mit gegeben werden muss um auf den Timer Zugriff zu erhalten.
Die eigentliche Animation läuft dann in einer Schleife ab die alle Felder in der aufzulösenden Zeile mit den Zahlen 1 bis 10 belegt, das Fenster zum neuzeichnen zwingt und dann kurz wartet um die Animation nicht zu schnell ablaufen zu lassen.

void CheckBombStones(HWND hwnd) { byte anim; [...] if (LineFull == TRUE) { //Zeile voll //Zeile animieren!!! KillTimer(hwnd, ID_TETRISTIMER); //erstmal Timer ausschalten for (anim=1;anim<11;anim++) { memset(&Tetris[i][1],anim, TBREITE-2);//eine Zeile auf einen Schlag belegen SendMessage(hwnd, WM_PAINT, 0, 0);//neuzeichnen erzwingen Sleep(30);//kurze Pause } //Timer nach der Animation wieder anschalten SetTimer(hwnd, ID_TETRISTIMER, GameSpeed, TetrisMove); //alle Zeilen die darüberliegen eine nach unten rücken [...] }
Das wars auch schon - hier ein kleiner Screenshot von dieser Animation:

Tetris mit explodierender Zeile

Unser Tetris-Spiel ist nun komplett. Zusätzliche Erweiterungen sind Ihnen Überlassen. Wenn Sie glauben, dass Sie eine gute Idee hatten, dann schreiben Sie uns und wir würden uns freuen Ihre Tetris-Versionen hier vor zustellen.

Hier gibts wie immer den Source der letzten Tetrisversion + zugehöriger Bilder als Download (zip-Datei)

Die Grundlegenden Prinzipien bei der Arbeit mit DirectDraw sollten nun klar sein. Mit diesem Wissen sollte es Ihnen jetzt möglich sein eigene kleine Anwednungen und Spielchen auf dar Basis von DirectDraw zu entwickeln. Die nächsten Kapitel werden sich intensiv mit der direkten und gezielten Manipulation von Bildpunkten unter DirectDraw beschäftigen. Unteranderem wird das Farbmodell genauer unter die Lupe genommen.