Teil 7: Sprites - Erweitert

TIGCC-Tutorial für Einsteiger.

Teil 7: Sprites - Erweitert

Beitragvon saubue am 6. Juli 2008 01:16

// Editierte Version: Dank an mhubi

Nachdem ihr nun mit den Grundsätzen vertraut seid gibt es nun ein etwas erweitertetes Beispiel, das uns weiter auf das Ziel dieses Einsteigerkurses vorbereiten soll: ein Snake-Spiel!
Das folgende wird eine SEHR einfache Snake-Version sein: ohne Highscore, ohne Boni und ohne Optionen. Einiges wird euch jetzt unbekannt sein oder auf den ersten Blick kompliziert erscheinen, aber keine Angst; im Folgenden wird alles geklärt werden.

Kommen wir also ohne Umschweife mit dem Projekt 'snake' zur Sache (natürlich nur eine einfache Version ;)):

Code: Alles auswählen
// C Source File
// Created 16.04.2005; 00:25:35


#include <tigcclib.h>

#define HEIGHT 5


// Das sind die möglichen Richtungen
enum directions {LEFT, RIGHT, UP, DOWN};


// Die Schlange (5x5)
unsigned char snake[5] = {
   0xF8,0xF8,0xF8,0xF8,0xF8
};

// Ein Apfel (5x5)
unsigned char apple[5] = {
   0x70,0x98,0xB8,0xF8,0x70
};

// Ein paar Globale Variablen
short apx, apy;      // Die Position des Apfels
short length;      // Die Länge der Schlange
short snakex[100], snakey[100];      // Die Positionen der Schlange (max. Länge 100)   


// Dies ist eine übliche Warte-Funktion
void Wait(short delay)
{
   short t1, t2;
   randomize();
   for (t1 = 0; t1 < delay; t1++)
      t2 = random(delay);
}


// Hier bestimmen wir zufällig die Position eines Apfels
// und stellen ihn auf dem Bildschirm dar
void PlaceApple(void)
{
   randomize();

   apx = random(LCD_WIDTH/5)*5;
   apy = random(LCD_HEIGHT/5)*5;
   
   Sprite8(apx, apy, HEIGHT, apple, LCD_MEM, SPRT_XOR);
}


// Hier wird die Schlange auf den Bildschirm gezeichnet
void DrawSnake(short xp, short yp)
{
   short i;
   
   Sprite8(snakex[length], snakey[length], HEIGHT, snake, LCD_MEM, SPRT_XOR);         
   
   for (i = (length+1); i > 0; i--) {
      snakex[i] = snakex[i-1];
      snakey[i] = snakey[i-1];
   }
   
   snakex[0] = xp;
   snakey[0] = yp;
   
   Sprite8(xp, yp, HEIGHT, snake, LCD_MEM, SPRT_XOR);
}


// Das ist die eigentliche Hauptfunktion des Spiels
void Game(void)
{
   short xp = -5, yp = 0, i;
   short direction = RIGHT;

   ClrScr();
   
   length = 4;

   // Hier wird die Schlange am Anfang gezeichnet (die Anfangslänge beträgt 4)
   for (i = 0; i < 5; i++) {
      xp += 5;
      snakex[4-i] = xp;
      snakey[4-i] = yp;
      Sprite8(xp, yp, HEIGHT, snake, LCD_MEM, SPRT_XOR);
   }

   PlaceApple();
   
   // Die Hauptschleife
   do {
      
      // Prüfen, ob die Schlange den Bildschirmrand berührt
      if (direction == RIGHT && xp > LCD_WIDTH-5)
         break;
      else if (direction == LEFT && xp < 0)
         break;
      else if (direction == UP && yp < 0)
         break;
      else if (direction == DOWN && yp > LCD_HEIGHT-5)
         break;
         
      // Tastenabfrage für die durch die Länge gegebene Dauer
      for (i = 0; i < 300-(length*5); i++) {
         if (_keytest(RR_LEFT) && direction != RIGHT)
            direction = LEFT;
         else if (_keytest(RR_RIGHT) && direction != LEFT)
            direction = RIGHT;
         else if (_keytest(RR_UP) && direction != DOWN)
            direction = UP;
         else if (_keytest(RR_DOWN) && direction != UP)
            direction = DOWN;
      
         else if (_keytest(RR_ESC))
            return;
      
         Wait(7);
      }
      
      // Weiterfahren in eine bestimmte Richtung
      if (direction == RIGHT)
         xp += 5;
      else if (direction == LEFT)
         xp -= 5;
      else if (direction == UP)
         yp -= 5;
      else if (direction == DOWN)
         yp += 5;
      
      // Prüfen, ob die Schlange einen Apfel gefressen hat
      if (xp == apx && yp == apy) {
         Sprite8(apx, apy, HEIGHT, apple, LCD_MEM, SPRT_XOR);
         length++;
         snakex[length] = snakex[length-1];
         snakey[length] = snakey[length-1];
         Sprite8(snakex[length], snakey[length], HEIGHT, snake, LCD_MEM, SPRT_XOR);
         PlaceApple();
      }
      
      // Prüfen ob die Schlange sich selbst gefressen hat
      for (i = 1; i <= length; i++) {
         if (xp == snakex[i] && yp == snakey[i])
            return;
      }
      
      DrawSnake(xp, yp);
         
   } while (TRUE);   
}


// Hauptfunktion (Programmbeginn)
void _main(void)
{
   LCD_BUFFER screen;   // Die Variable für die Sicherung des Bildschirminhalts
   INT_HANDLER saveint1, saveint5;
   
   // Der Bildschirminhalt wird gespeichert
   LCD_save(screen);   
   // Die Interrupt-Handler werden gespeichert
   saveint1 = GetIntVec(AUTO_INT_1);
   saveint5 = GetIntVec(AUTO_INT_5);
   SetIntVec(AUTO_INT_1, DUMMY_HANDLER);
   SetIntVec(AUTO_INT_5, DUMMY_HANDLER);
   
   
   // Das Spiel wird gestartet
   Game();
   
   // Die Interrupt-Handler werden wieder hergestellt
   SetIntVec(AUTO_INT_1, saveint1);
   SetIntVec(AUTO_INT_5, saveint5);
   // Der Bildschirminhalt wird wieder hergestellt und eine abschließnde
   // Nachricht wird in die Statuszeile geschrieben
   LCD_restore(screen);
   ST_helpMsg("Snake v0.1 by XXXXXXX");
}


OK, zuerst wird natürlich gezockt, aber wenn ihr euch ausgelebt habt, müssen wir mit der Analyse beginnen:

Na gut, noch nicht richtig. Zuerst sollten wir die Bedeutung der globalen Variablen und das Spielprinzip an sich klären.
In diesem Programm wird das "Spielfeld" in ein Raster aus 5 mal 5 Pixel großen Feldern eingeteilt. Der Schlange-Sprite ist deswegen, genau wie der Apfel-Sprite, 5 auf 5 Pixel groß.
Dass die Schlange länger wird, erreichen wir durch die Varibelen 'snakex' und 'snakey'. Das sind Listen, in denen die Positionen der einzelnen Schlangenstücke gespeichert sind (in 'snakex' die x-Position und in 'snakey' die y-Position). Diese werden mit jeder Bewegung weitergeschoben und das letzte Stück (an der Stelle 'length') wird immer gelöscht (in der Liste sowie auf dem Bildschirm). Die Listen haben eine Größe von 100, d.h. wird die Schlange länger als 100 (was jedoch meiner Ansicht nach recht unwahrscheinlich ist) wird sich das Programm aufhängen. Reicht euch das nicht, könnt ihr sie selbstverständlich auch länger machen.

Wir werden nun so vorgehen, wie es das Programm auch tut, fangen also mit der _main-Funktion an.
Hier gibt es nicht viel zu erklären, außer vielleicht die Geschichte mit dem Bildschirminhalt. Den speichern wir hier eigentlich nur selbst, um am Schluss die Nachricht in die Statuszeile zu bringen (natürlich muss für XXXXXXX noch euer Name eingesetzt werden :)). Die automatische Speicherung deaktiviert ihr wie folgt:
Menüleiste->Project->Options->Compilation->Program Options...->Home Screen und dann Save/Resore LCD contents deaktivieren.

Die Sache mit den Interrupt-Handlern haben wir ja schon in der vorigen Lektion besprochen; das sollte also klar sein.

Als nächstes wird die Funktion 'Game()' aufgerufen, die wir jetzt näher betrachten wollen.

Code: Alles auswählen
// Das ist die eigentliche Hauptfunktion des Spiels
void Game(void)
{
   short xp = -5, yp = 0, i;
   short direction = RIGHT;

   ClrScr();

   length = 4;

   // Hier wird die Schlange am Anfang gezeichnet (die Anfangslänge beträgt 4)
   for (i = 0; i < 5; i++) {
      xp += 5;
      snakex[4-i] = xp;
      snakey[4-i] = yp;
      Sprite8(xp, yp, HEIGHT, snake, LCD_MEM, SPRT_XOR);
   }

   PlaceApple();


Das ist sozusagen das Intro: die Variablen, die benötigt werden, bekommen ihre Startwerte, der Bildschirminhalt wird gelöscht, die Schlange startet am oberen rechten Bildschirmrand (sollte euch die Bedeutung der Werte unklar sein, dann verändert sie mal) und der ersten Apfel wird mit 'PlaceApple()' gesetzt. Das geschieht wie folgt:

Code: Alles auswählen
// Hier bestimmen wir zufällig die Position eines Apfels
// und stellen ihn auf dem Bildschirm dar
void PlaceApple(void)
{
   randomize();

   apx = random(LCD_WIDTH/5)*5;
   apy = random(LCD_HEIGHT/5)*5;
   
   Sprite8(apx, apy, HEIGHT, apple, LCD_MEM, SPRT_XOR);
}


Der Zufallsgenerator wird also aktiviert, die globalen Variablen 'apx' und 'apy', die die Position eines Apfes beschreiben, werden zufällig bestimmt und abschließend wird der Apfel-Sprite auf den Bildschirm gezeichnet. Das /5 und *5 bewirken, dass nur alle 5 Pixel ein Apfel gesetzt werden kann.

Weiter geht's in der 'Game()'-Funktion:

Code: Alles auswählen
   // Die Hauptschleife
   do {
      
      // Prüfen, ob die Schlange den Bildschirmrand berührt
      if (direction == RIGHT && xp > LCD_WIDTH-5)
         break;
      else if (direction == LEFT && xp < 0)
         break;
      else if (direction == UP && yp < 0)
         break;
      else if (direction == DOWN && yp > LCD_HEIGHT-5)
         break;
         
      // Tastenabfrage für die durch die Länge gegebene Dauer
      for (i = 0; i < 300-(length*5); i++) {
         if (_keytest(RR_LEFT) && direction != RIGHT)
            direction = LEFT;
         else if (_keytest(RR_RIGHT) && direction != LEFT)
            direction = RIGHT;
         else if (_keytest(RR_UP) && direction != DOWN)
            direction = UP;
         else if (_keytest(RR_DOWN) && direction != UP)
            direction = DOWN;
      
         else if (_keytest(RR_ESC))
            return;
      
         Wait(7);
      }
      
      // Weiterfahren in eine bestimmte Richtung
      if (direction == RIGHT)
         xp += 5;
      else if (direction == LEFT)
         xp -= 5;
      else if (direction == UP)
         yp -= 5;
      else if (direction == DOWN)
         yp += 5;
      
      // Prüfen, ob die Schlange einen Apfel gefressen hat
      if (xp == apx && yp == apy) {
         Sprite8(apx, apy, HEIGHT, apple, LCD_MEM, SPRT_XOR);
         length++;
         snakex[length] = snakex[length-1];
         snakey[length] = snakey[length-1];
         Sprite8(snakex[length], snakey[length], HEIGHT, snake, LCD_MEM, SPRT_XOR);
         PlaceApple();
      }
      
      // Prüfen ob die Schlange sich selbst gefressen hat
      for (i = 1; i <= length; i++) {
         if (xp == snakex[i] && yp == snakey[i])
            return;
      }
      
      DrawSnake(xp, yp);
         
   } while (TRUE);   
}


Das scheint etwas viel zu sein, also nehmen wir es auseinander:

Code: Alles auswählen
   // Die Hauptschleife
   do {
      
      // Prüfen, ob die Schlange den Bildschirmrand berührt
      if (direction == RIGHT && xp > LCD_WIDTH-5)
         break;
      else if (direction == LEFT && xp < 0)
         break;
      else if (direction == UP && yp < 0)
         break;
      else if (direction == DOWN && yp > LCD_HEIGHT-5)
         break;


In der Hauptschleife bewegen wir uns eigentlich die meiste Zeit. Zuerst prüfen wir alos, ob die Schlange den Bildschirmrand berührt (besser gesagt, ihn verlässt), was natürlich von der momentanen Richung, die durch die Variable 'direc' beschrieben wird, abhängt. Ist dies der Fall, wird die Hauptschleife mit break unterbrochen; wir kehren zurück zur _main-Funktion und das Spiel ist beendet.

Code: Alles auswählen
      // Tastenabfrage für die durch die Länge gegebene Dauer
      for (i = 0; i < 300-(length*5); i++) {
         if (_keytest(RR_LEFT) && direction != RIGHT)
            direction = LEFT;
         else if (_keytest(RR_RIGHT) && direction != LEFT)
            direction = RIGHT;
         else if (_keytest(RR_UP) && direction != DOWN)
            direction = UP;
         else if (_keytest(RR_DOWN) && direction != UP)
            direction = DOWN;
      
         else if (_keytest(RR_ESC))
            return;
      
         Wait(7);
      }


Natürlich wird die Schlange vom User gesteuert, und das geschieht in der for-Schleife. Wie lange der User Zeit hat, wird durch die Länge der Schlang bestimmt: je länger, desto weniger Zeit bleibt und desto schneller wird das Spiel. Der Wert 300 sowie der Wert 7 für die 'Wait()'-Funktion sind willkürlich festgelegt; ihr könnt sie nach Belieben verändern. Wird eine Taste gedrückt, wird durch die UND-Bedingung zusätzlich gewährleistet, dass man nicht in die entgegengesetzte Richtung fahren darf. Wird [ESC] gedrückt, wird die Funktion mit return verlassen und das Spiel ist wiederum beendet.

Code: Alles auswählen
      // Weiterfahren in eine bestimmte Richtung
      if (direction == RIGHT)
         xp += 5;
      else if (direction == LEFT)
         xp -= 5;
      else if (direction == UP)
         yp -= 5;
      else if (direction == DOWN)
         yp += 5;


Nun wird in die vom User bestimmte Richtung weitergefahren, und zwar 5 Pixel, da ein Schlangenteil 5 auf 5 Pixel groß ist.

Code: Alles auswählen
      // Prüfen, ob die Schlange einen Apfel gefressen hat
      if (xp == apx && yp == apy) {
         Sprite8(apx, apy, HEIGHT, apple, LCD_MEM, SPRT_XOR);
         length++;
         snakex[length] = snakex[length-1];
         snakey[length] = snakey[length-1];
         Sprite8(snakex[length], snakey[length], HEIGHT, snake, LCD_MEM, SPRT_XOR);
         PlaceApple();
      }


Wenn die Schlange einen Apfel gefressen hat (ihre Position also mit der des Apfels übereinstimmt), wird zuerst der Apfel-Sprite gelöscht. Danach wird die Länge um 1 erhöht.Anschließend wird die letzte Position in den Listen um eins nach hinten verschoben, da das letzte Schlangenteil ja noch einmal stehen bleiben muss (so wird die Schlange um 1 länger). Anschließend wird natürlich noch ein neuer Apfel gesetzt.

Code: Alles auswählen
      // Prüfen ob die Schlange sich selbst gefressen hat
      for (i = 1; i <= length; i++) {
         if (xp == snakex[i] && yp == snakey[i])
            return;
      }
      
      DrawSnake(xp, yp);
         
   } while (TRUE);


Jetzt überprüfen wir, ob die Schlange sich selbst gefressen hat, in dem wir alle momentanen Positionen der Teile (die ja in den Listen vermerkt sind) mit der aktuellen Position vergleichen. Fällt dieser Vergleich positiv aus, wird das Spiel beendet.
Als nächstes rufen wir die Funktion 'DrawSnake()' mit den neuen Koordinatn des Schlangenkopfes auf, bevor sich die Schleife wiederholt.

Code: Alles auswählen
// Hier wird die Schlange auf den Bildschirm gezeichnet
void DrawSnake(short xp, short yp)
{
   short i;
   
   Sprite8(snakex[length], snakey[length], HEIGHT, snake, LCD_MEM, SPRT_XOR);         
   
   for (i = (length+1); i > 0; i--) {
      snakex[i] = snakex[i-1];
      snakey[i] = snakey[i-1];
   }
   
   snakex[0] = xp;
   snakey[0] = yp;
   
   Sprite8(xp, yp, HEIGHT, snake, LCD_MEM, SPRT_XOR);
}


Zuerst einmal wird das letzte Teil gelöscht, dessen Position wir aus dem letzten Eintrag der Listen, welcher sich immer an der Position 'length' befindet, bekommen. Dann werden alle Positionen um eine Stelle nach hinten verschoben, da die Schlange ich ja weiterbewegt. Der erste Listeneintrag bekommt jeweils die aktuelle Position, die als Parameter übergeben wurde. Dort wird dann der Schlangen-Sprite erneut gezeichnet.

Das war bereits das gesamte Spiel! Gut, für die meisten wird wohl immer noch nicht alles klar sein, aber wie in den anderen Lektionen bereits erwähnt wird einem Vieles durch das "Trial-Error-Prinzip" verständlich: Also einfach mal rumprobieren. Wie wäre es zum Beispiel mit einer optionalen Punkteanzeige?

Viel Spass beim rumwerkeln!
"All animals are equal. But some animals are more equal than others." - George Orwell (Animal Farm)

boolsoft - software development for texas instruments 68k calculators
Benutzeravatar
saubue
Administrator
Administrator
 
Beiträge: 1434
Registriert: 11. März 2005 13:55
Wohnort: Freiburg im Breisgau

Re: Teil 7: Sprites - Erweitert

Beitragvon saubue am 11. August 2008 23:51

Beitrag von Boscop:

Warum ist das Snake-Spiel nach ca. 1 Minute spielen fast doppelt so schnell? (Irgendwie tut's dann die Wait()- Funktion nicht mehr, aber warum?)
"All animals are equal. But some animals are more equal than others." - George Orwell (Animal Farm)

boolsoft - software development for texas instruments 68k calculators
Benutzeravatar
saubue
Administrator
Administrator
 
Beiträge: 1434
Registriert: 11. März 2005 13:55
Wohnort: Freiburg im Breisgau

Re: Teil 7: Sprites - Erweitert

Beitragvon saubue am 11. August 2008 23:52

Beitrag von Stefan Heule:

Doch, die wait-Funktion geht weiterhin wunderbar, und es hängt auch nicht davon ab, wie lange du bereits gespielt hast, sondern wie lange dein Schlange ist ;) Sieh dir mal folgenden Code an:

Code: Alles auswählen
      // Tastenabfrage für die durch die Länge gegebene Dauer
      for (i = 0; i < 300-(length*5); i++) {
         if (_keytest(RR_LEFT) && direction != RIGHT)
            direction = LEFT;
         else if (_keytest(RR_RIGHT) && direction != LEFT)
            direction = RIGHT;
         else if (_keytest(RR_UP) && direction != DOWN)
            direction = UP;
         else if (_keytest(RR_DOWN) && direction != UP)
            direction = DOWN;
       
         else if (_keytest(RR_ESC))
            return;
       
         Wait(7);
      }


Besonderes Augenmerk auf for (i = 0; i < 300-(length*5); i++). Das heisst, je länger die Schlange ist, deste weniger oft wird die Schleife ausgeführt, und deshalb wird das Spiel schneller.

Es gibt noch einen lustigen Bug: Es kann sein, dass der Apfel genau dort entsteht, wo der alte Apfel war, und das sieht dann so aus, als hätte man den Apfel nicht gefressen. Dem ist aber nicht so, nur der neue ist an der selben Stelle. Dieses Tutorial soll aber eh nicht ein fertiges Game abliefern, sondern auch dazu anreizen, das Spiel selbst weiterzuentwickeln, und nun habt ihr einen Grund ;)
"All animals are equal. But some animals are more equal than others." - George Orwell (Animal Farm)

boolsoft - software development for texas instruments 68k calculators
Benutzeravatar
saubue
Administrator
Administrator
 
Beiträge: 1434
Registriert: 11. März 2005 13:55
Wohnort: Freiburg im Breisgau

Re: Teil 7: Sprites - Erweitert

Beitragvon saubue am 11. August 2008 23:52

Beitrag von Boscop:

Das die Abfragedauer von der Länge abhängt, muss ich wohl übersehen haben, thx ;-)
"All animals are equal. But some animals are more equal than others." - George Orwell (Animal Farm)

boolsoft - software development for texas instruments 68k calculators
Benutzeravatar
saubue
Administrator
Administrator
 
Beiträge: 1434
Registriert: 11. März 2005 13:55
Wohnort: Freiburg im Breisgau


Zurück zu Grundlagen: TIGCC

Wer ist online?

Mitglieder in diesem Forum: 0 Mitglieder und 1 Gast