Sprites - Erweitert


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):

// C Source File 
// Created 16.04.2005; 00:25:35 


#include  

#define HEIGHT 5 


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


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

// Ein Apfel (5x5) 
unsigned char apple[8] = { 
   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.

// 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:

// 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:

   // 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:

   // 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.

      // 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.

      // 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.

      // 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.

      // 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.

// 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!