Kleiner JavaScript Kurs

Spiel "Flappy Bird"

Das Spiel habe ich in Anlehnung an den OpenHPI - Kurs "Spieleentwicklung mit JavaScript: Flappy Bird" entwickelt. Als "Spielfläche verwenden wir ein Canvas - Element (mit Rahmen und Hintergrundfarbe). Die Steuerung erfolgt mit der Tastatur. Ggf. benötigen wir später auch noch einen Button zum Starten des Spiels.

<!DOCTYPE html>
<html lang="de">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Flappy Bird</title>
        <style>
        	canvas {
              border: 1px solid black; 
              background-color: yellow;}
        </style>
    </head>                    
    <body>
		<canvas id="canvas" width="480" height="640"></canvas>
    </body>
</html>

Um die Programmierung des Spiels zu vereinfachen, werden wir alle Spiel - Elemente (Vogel / bird, Röhren / pipe) als Objekte programmieren


Die Röhre (pipe) als Objekt (mit den Eigenschaften x, width, height, speed und den Methoden draw() und update():
// Die Röhre als Objekt (pipe)
    const pipe = {
            x: 350,
            width: 50,
            height: 150,
            speed: -5,
            draw() {
                ctx.fillRect(this.x, 0, this.width, this.height);
            },
            update() {
                this.x += this.speed
            }  
      };
// Das Hauptprogramm (game - loop)
      function loop() {
          ctx.clearRect(0, 0, screenWidth, screenHeight);

          pipe.draw();
          pipe.update();

          window.requestAnimationFrame(loop);
        }

        window.requestAnimationFrame(loop);

Als nächstes wollen wir 3 Röhren von rechts nach links bewegen.Dazu schreiben wir (mit einer for - Schleife) 3 pipe - Objekte in ein pipes Array.

const canvas = document.getElementById('canvas');
            const ctx = canvas.getContext('2d');
            const screenWidth = canvas.clientWidth;
            const screenHeight = canvas.clientHeight; 
      		
            const pipes = [];
            const pipeCount = 3; 
            for(let i=0; i < pipeCount; i++){
              pipes.push({	x: screenWidth + i * screenWidth / pipeCount,
                            width: 50,
                            height: 150,
                            speed: -5,
                            draw() {
                                ctx.fillRect(this.x, 0, this.width, this.height);
                            },
                            update() {
                                this.x += this.speed
                            }  
                      });
            }  
           
          function loop() {
              ctx.clearRect(0, 0, screenWidth, screenHeight);

              for (let i = 0; i < pipeCount; i++) {
              pipes[i].draw();
              pipes[i].update();
            	}

              window.requestAnimationFrame(loop);
            }

            window.requestAnimationFrame(loop);

Die Funktion zum Zeichnen eines Elements (Quadrat, 10 x 10 Pixel) könnte so aussehen:

function snakeElementZeichnen(x,y){
ctx.fillStyle = "blue";
ctx.fillRect(x, y, zellenBreite, zellenHoehe);
}

Diese Funktion wird von der Funktion "snakeZeichnen()" verwendet

function snakeZeichnen(){
    for(var i=0; i<snake.length; i++){
      snakeElementZeichnen(snake[i][0],snake[i][1]);
    }
}

Wir rufen die Funktion zum Testen einmal auf:

snakeZeichnen();
Snake zeichnen

Als nächstes wollen wir die Snake bewegen. Dazu definieren wir 2 globale Variable "richtungX" und "richtungY". Zum Bewegen der Snake löschen wir zunächst das letzte Element des Snake - Arrays:
snake.pop();
Dann berechnen wir die Position des neuen Kopfes (zum Test 10 Pixel nach rechts) und fügen den neuen Kopf am Anfang des Arrays ein:
snake.unshift(kopfNeu);
Jetzt müssen wir nur noch die Funktion snakeBewegen z.B. alle 250ms wiederholen:
var intervalID = window.setInterval(snakeBewegen, 250);

var richtungX=10;
var richtungY=0;
function snakeBewegen(){
    ctx.fillStyle = "lightgreen";
    ctx.fillRect(0, 0, breite, hoehe);
    snake.pop();
    kopfNeu=[snake[0][0]+richtungX,snake[0][1]+richtungY];
    snake.unshift(kopfNeu);
    snakeZeichnen();
    }
var intervalID = window.setInterval(snakeBewegen, 250);

Für die Tastatursteuerung benötigen wir einen Event-Listener, der beim Drücken einer Taste die angegebene Funktion (hier tastaturSteuerung) startet:
document.addEventListener('keydown', tastaturSteuerung);
und eine Funktion, die die Tastendrücke mit entsprechenden if - Anweisungen auswertet:
var tastenDruck=event.keyCode;

document.addEventListener('keydown', tastaturSteuerung);
function tastaturSteuerung(event){
    const tasteLinks=37;
    const tasteRechts=39;
    const tasteOben=38;
    const tasteUnten=40;
    const tasteESC=27;
    var tastenDruck=event.keyCode;
    if(tastenDruck==tasteLinks){
      richtungX=-10;
      richtungY=0;
    }
    if(tastenDruck==tasteRechts){
      richtungX=10;
      richtungY=0;
    }
    if(tastenDruck==tasteOben){
      richtungX=0;
      richtungY=-10;
    }
    if(tastenDruck==tasteUnten){
      richtungX=0;
      richtungY=10;
    }
    if(tastenDruck==tasteESC){
      window.clearInterval(intervalID);
    }
}

Als nächstes wollen wir das Futter für die Snake auf der Spielfläche anzeigen.Wir brauchen 2 globale Variable für die Koordinaten des Futters (futterX und futterY; der Bildpunkt oben links, obwohl das Futter als Kreis dargestellt wird - dadurch sind wir auch beim Futter im gleichen Raster und können viel einfacher eine Kollision zwischen Snake und Futter erkennen). Da das Futter an einer zufälligen Stelle auf der Spielfläche erscheinen soll, müssen wir die Position mit dem Zufallsgenerator (Math.random) ermitteln. Wenn das Futter gefressen ist, muss neues Futter an einer anderen Stelle erscheinen. Das steuern wir mit der globalen Variablen "gefressen". Beim ersten Start des Spiels ist gefressen=true, so dass die ersten Koordinaten für das Futter ermittelt werden. Sobald die Koordinaten festliegen, wird gefressen=false. Das Fressen bleibt damit an dieser Stelle, bis es gefressen wird. Die Funktion futterZeichnen muß am Ende der "Game - Loop" (snakeBewegen) eingefügt werden.

var futterX;
var futterY;
var gefressen=true;
function futterZeichnen(){
    if(gefressen){
      futterX=Math.floor(Math.random()*breite/zellenBreite)*zellenBreite;
      futterY=Math.floor(Math.random()*hoehe/zellenHoehe)*zellenHoehe;
      gefressen=false;
    }
    ctx.beginPath();
    ctx.arc(futterX+zellenBreite/2, futterY+zellenHoehe/2, zellenBreite/2, 0, Math.PI*2);
    ctx.fillStyle = "red";
    ctx.fill();
    ctx.closePath();
    ctx.stroke();
}
Snake Futter zeichnen

Um festzustellen, ob die Snake das Futter frisst, müssen wir die x- und y-Koordinaten vom Futter (futterX, futterY) mit den Koordinaten des Snake - Kopfes (snake[0][0], snake[0][1] ) vergleichen. Sind die Koordinaten gleich, wird die Variable gefressen auf true gesetzt und dadurch neues Futter an einer zufälligen Stelle erzeugt. Damit die Schlange beim Fressen wächst, fügen wir mit snake.push([0,0]) ein neues Element ein. Die x/y Werte spielen dabei keine Rolle, da bei der nächsten Snake Bewegung das letzte Element des Arrays gelöscht wird.

function futterGefressen(){
    if(futterX==snake[0][0] & futterY==snake[0][1]){
      gefressen=true;
      snake.push([0,0]);
    }
}

GAME OVER
Wir hatten bei der Definition der ESC Tastenfunktion schon die Beendigung der "game loop" programmiert. Da wir aber noch an anderen Stellen das Programm beenden müssen, lagern wir das Programmende in eine eigene Funktion aus:

function ende(){
    window.clearInterval(intervalID);
    ctx.font = "32px Georgia";
    ctx.fillText("Game Over", breite/2, hoehe/2);
}

Das Spiel soll auch beendet werden, wenn
  • der Rand des Spielfelds von der Snake überschritten wird
  • die Snake sich selbst "frisst"

function gameOver(){
    if(snake[0][0]<0 || snake[0][0]>=breite || snake[0][1]<0 || snake[0][1]>=hoehe){
      ende();
    }  
    for(var i=1; i<snake.length; i++){
      if(snake[0][0]==snake[i][0] && snake[0][1]==snake[i][1]){
        ende();
      }
    }  
}

So könnte das Snake - Spiel aussehen: Snake - Spiel


Hier die HTML-Seite mit dem kompletten Programm


<!DOCTYPE html>
<html lang="de">
    <head>
        <meta charset="utf-8">
                        
    </head>
                        
    <body>
		<canvas id="myCanvas" width="600" height="600"  
          style="border:2px solid #000000; background-color: lightgreen;"></canvas><br>
    </body>
  	<script>
      var canvas = document.getElementById("myCanvas");
      var ctx = canvas.getContext("2d");
      var breite = canvas.width;
      var hoehe = canvas.height;
      var zellenHoehe=10;
      var zellenBreite=10;
      for(var i=0; i<breite; i=i+zellenBreite){
        for(var j=0; j<hoehe; j=j+zellenHoehe){
          ctx.rect(i,j,zellenBreite,zellenHoehe);  
        }
      }
      ctx.stroke();
      var snake=[[300,300],[290,300],[280,300],[270,300]];
      function snakeElementZeichnen(x,y){
        ctx.fillStyle = "blue";
        ctx.fillRect(x, y, zellenBreite, zellenHoehe);
      }
      function snakeZeichnen(){
        for(var i=0; i<snake.length; i++){
          snakeElementZeichnen(snake[i][0],snake[i][1]);
        }
      }
      var futterX;
      var futterY;
      var gefressen=true;
      function futterZeichnen(){
        if(gefressen){
          futterX=Math.floor(Math.random()*breite/zellenBreite)*zellenBreite;
          futterY=Math.floor(Math.random()*hoehe/zellenHoehe)*zellenHoehe;
          gefressen=false;
        }
        ctx.beginPath();
    	ctx.arc(futterX+zellenBreite/2, futterY+zellenHoehe/2, zellenBreite/2, 0, Math.PI*2);
        ctx.fillStyle = "red";
		ctx.fill();
		ctx.closePath();
    	ctx.stroke();
      }
      function futterGefressen(){
        if(futterX==snake[0][0] & futterY==snake[0][1]){
          gefressen=true;
          snake.push([0,0]);
          //alert(snake);
        }
      }
      function gameOver(){
        if(snake[0][0]<0 || snake[0][0]>=breite || snake[0][1]<0 || snake[0][1]>=hoehe){
          ende();
        }  
        for(var i=1; i<snake.length; i++){
          if(snake[0][0]==snake[i][0] && snake[0][1]==snake[i][1]){
            ende();
          }
        }  
      }
      function ende(){
        window.clearInterval(intervalID);
        ctx.font = "32px Georgia";
        ctx.fillText("Game Over", breite/2, hoehe/2);
      }
      //snakeZeichnen();
      var richtungX=10;
      var richtungY=0;
      function snakeBewegen(){
        ctx.fillStyle = "lightgreen";
  		ctx.fillRect(0, 0, breite, hoehe);
        snake.pop();
        kopfNeu=[snake[0][0]+richtungX,snake[0][1]+richtungY];
        snake.unshift(kopfNeu);
        snakeZeichnen();
        futterGefressen()
        futterZeichnen();
        gameOver();
      }
      var intervalID = window.setInterval(snakeBewegen, 250);
      document.addEventListener('keydown', tastaturSteuerung);
      function tastaturSteuerung(event){
        const tasteLinks=37;
        const tasteRechts=39;
        const tasteOben=38;
        const tasteUnten=40;
        const tasteESC=27;
        var tastenDruck=event.keyCode;
        if(tastenDruck==tasteLinks){
          richtungX=-10;
          richtungY=0;
        }
        if(tastenDruck==tasteRechts){
          richtungX=10;
          richtungY=0;
        }
        if(tastenDruck==tasteOben){
          richtungX=0;
          richtungY=-10;
        }
        if(tastenDruck==tasteUnten){
          richtungX=0;
          richtungY=10;
        }
        if(tastenDruck==tasteESC){
          window.clearInterval(intervalID);
        }
      }
	</script>
</html>   

Weiterentwicklung des Programms
Der HTML - Seite fehlt noch eine Überschrift und eine kurze Anleitung. Außerdem könnte man einen Punktezähler (Anzeige in einem html Tag, z.b. <p></p> oder <div></div> ) programmieren, der die Zahl der gefressenen "Futter" zählt. Das Programm hat auch noch einen "Bug": beim Erzeugen von neuem Futter wird nicht verhindert, dass das Futter auf einer Koordinate der Snake landet (ist bei mir noch nicht vorgekommen, kann aber passieren)