HTML5-Tutorium: Canvas: MiniPong 03

aus GlossarWiki, der Glossar-Datenbank der Fachhochschule Augsburg

Dieser Artikel erfüllt die GlossarWiki-Qualitätsanforderungen nur teilweise:

Korrektheit: 3
(zu größeren Teilen überprüft)
Umfang: 3
(einige wichtige Fakten fehlen)
Quellenangaben: 5
(vollständig vorhanden)
Quellenarten: 5
(ausgezeichnet)
Konformität: 5
(ausgezeichnet)

HTML-Tutorium: MiniPong

MiniPong: | Teil 1 | Teil 2 | Teil 3 | Teil 4 | Teil 5

Musterlösung: index.html (WK_MiniPong03 (SVN))
index_h.html, index_v.html (WK_MiniPong03a (SVN))
Die Steuerung erfolgt mit den Pfeiltasten. Das zweite Paddle wird mit den Tasten „w“ und „s“ gesteuert.

Ziel: Interaktion mit dem Schläger

Im dritten Teil des Tutoriums wird beschrieben, wie man die Interaktion mit dem Schläger realisiert.

Neues Projekt anlegen

Legen Sie ein neues Projekt mit dem Namen „MiniPong03“ an.

Kopieren Sie die Dateien der App3 des Projektes MiniPong02 (App3) in das neue Projekt.

Nehmen Sie folgende Anpassungen vor:

  • Umbenennung des Ordners „app3“ in „app“.
  • Umbenennung der Datei „main3.js“ in „main.js“.
  • Umbenennung der Datei „index3.html“ in „index.html“.
  • In der Datei „main.js“:
    • Ersetzen von „app: 'app3'“ durch „app: 'app'“.
  • In der Datei „index.html“:
    • Ändern des Titels in „MiniPong03“:
    • Ersetzen von „js/main3“ durch „js/main“ im Script-Befehl.

Nun sollte die Anwendung wieder laufen.

Initialisierung

Als nächstes müssen die Schlägereigenschaften in die Initialisierungs-Datei „init.json“ eingefügt werden.

In das Objekt „model“ werden folgende Daten eingefügt:

"paddle":
{
  "width":    50,
  "height":    8,
  "pos":     {"x": 175, "y": 287},
  "vel":     {"x": 100, "y": 0},
  "acc":     {"x": 500, "y": 0}
}

Der Schläger wird durch ein Rechteck dargestellt. Dieses Rechteck ist 50 Pixel breit und 8 Pixel hoch. Es wird mittig in der Nähe des unteren Randes der Zeichenbühne platziert:

  • $($Breite des Canvas $-$ Breite des Schlägers$)/2 = (400-50)/2 = 175$
  • $($Höhe des Canvas $-$ Höhe des Schlägers$\,-\,5) = 300- 8 - 5 = 287$

Wenn der Benutzer durch Tastendruck den Schläger in Bewegung setzt, bewegt er sich zunächst mit eine Geschwindigkeit von 100 Pixeln pro Sekunde entlang der $x$-Achse. Je länger der Benutzer die Taste drück, desto schneller wird der Schläger. Er wird mit 500 Pixeln pro Sekunde beschleunigt.

In das Objekt „view“ werden folgende Daten eingefügt:

"paddle":
{
  "color":       "#aa55cc",
  "borderWidth": 1,
  "borderColor": "#000000"
}

Die View-Informationen unterscheiden sich nicht sonderlich von den Ball-View-Daten, außer, dass der Schläger eine andere Farbe haben soll.

Als letztes fügen wir hinter das View-Objekt noch ein neues Objekt ein:

"control":
{
  "player":
  {
    "left":  {"key": "ArrowLeft",  "keyCode": 37},
    "right": {"key": "ArrowRight", "keyCode": 39}
  }
}

Das Control-Objekt enthält Informationen zu den Steuermöglichkeiten, die der Spieler hat. Hier werden die Key-Codes der Tasten angegeben, mit denen der Benutzer den Schläger nach links bzw. rechts bewegen kann.

Modell des Schlägers

Das Model des Schlägers ist ähnlich aufgebaut, wie das Modell des Balls. Da das Paddle durch ein Rechteck dargestellt wird, gibt es natürlich keinen Radius, sondern eine Breite und eine Höhe, die gespeichert werden müssen. Außerdem werden nicht nur Positions- und Geschwindigkeitsangaben gespeichert, sondern auch Beschleunigungsinformationen gespeichert. Ein weiterer wesentlicher unterschied ist, dass der Konstruktor die Geschwindigkeits- und Beschleunigungsdaten nicht in den Attributen „vx“, „vy“, „ax“ und „ay“ speichert, sondern in den Attributen „vx_start“, „vy_start“, „ax_start“ und „ay_start“.

Der Grund ist, dass der Schläger sich zunächst gar nicht bewegt. Zu Beginn sind die Geschwindigkeit und die Beschleunigung also gleich 0. Erst, wenn der Benutzer den Schläger in Bewegung setzt, werden die Startwerte für Geschwindigkeit und Beschleunigung in die eigentlichen Geschwindigkeits- und Beschleunigungsattribute kopiert. Und wenn der Benutzer den Schläger stoppt, werden Geschwindigkeit und Beschleunigung wieder auf Null gesetzt.

Der Konstruktor sieht daher folgendermaßen aus:

function ModelPaddle(p_init_model)
{
  // model
  var l_pos = p_init_model.pos,
      l_vel = p_init_model.vel,
      l_acc = p_init_model.acc;

  this.width  = p_init_model.width;
  this.height = p_init_model.height;

  this.x = l_pos.x;
  this.y = l_pos.y;

  this.vx_start = l_vel.x;
  this.vy_start = l_vel.y;

  this.ax_start = l_acc.x;
  this.ay_start = l_acc.y;

  this.stop(); // By default, the paddle does not move.
}

Um den Schläger starten und stoppen zu können, werden die beiden Methoden „start“ und .„stop“ definiert. Für den Ball ist dies nicht notwendig, da sich dieser selbstständig bewegt und nicht durch Benutzeraktionen gesteuert wird.

Die Methode „stop“ setzte die Geschwindigkeits- und Beschleunigungsattribute des Schlägers auf 0. Sie insbesondere auch vom Konstruktor aufgerufen, um die Attribute „vx“, „vy“, „ax“ und „ay“ zu initialisieren. Zum Zeitpunkt der Erzeugung bewegt sich der Schläger noch nicht, also ist der Wert dieser Attribute jeweils Null.

ModelPaddle.prototype.stop =
 function()
 { this.vx = 0;
   this.vy = 0;

   this.ax = 0;
   this.ay = 0;
 };

Die Start-Methode hat die Aufgabe, die Geschwindigkeit- und die Beschleunigungsattribute des Schlägers auf die Anfangswerte zu setzen, sobald sie aufgerufen wird. Allerdings ist dies nicht so einfach, wie im Falle der Stopp-Methode.

FOLGENDES FUNKTIONIERT DAHER NICHT:

ModelPaddle.prototype.start =
 function()
 { this.vx = this.vx_start;
   this.vy = this.vy_start;

   this.ax = this.ax_start;
   this.ay = this.ay_start;
 };

Zum einen soll der Schläger, wenn er einmal gestartet wurde, konstant schneller werden. Allerdings wird die Start-Methode üblicherweise öfters als einmal aufgerufen. Wenn der Benutzer beispielsweise den Schläger mit Hilfe von Pfeiltasten steuert, hält der die entsprechende Taste längere Zeit gedrückt. Nach kurzer Zeit setzt die Tastenwiederholungsfunktion des Betriebssystems ein und der Startbefehl wird regelmäßig erneut abgesetzt. Und jeder erneute Aufruf des Startbefehls hätte zur Folge, dass die Geschwindigkeit des Schlägers zurück auf die Startgeschwindigkeit gesetzt wird.

Um dies zu verhindern, muss überprüft werden, ob der Schläger gerade in Bewegung ist oder nicht. Nur im letzteren Fall darf der Schläger mit Hilfe der Start-Werte in Bewegung gesetzt werden.

Es gibt noch einen zweiten Punkt, der berücksichtigt werden muss: Der Benutzer kann bei einem horizontalen Schläger wählen, ob der Schläger nach links oder nach rechts bewegt werden soll. Bei einem vertikalen Schläger hat er die Wahl zwischen rauf und runter.

Die Start-Methode, die nur die horizontalen Richtungsänderungen unterstützt, sieht folgendermaßen aus:

ModelPaddle.prototype.start =
  function(p_direction)
  {
    // react only if the paddle is not already moving
    if (this.vx === 0 && this.vy === 0)
    {
      switch (p_direction)
      {
        case "left":
          this.vx = -this.vx_start;
          this.ax = -this.ax_start;
          break;
        case "right":
          this.vx =  this.vx_start;
          this.ax =  this.ax_start;
          break;

        case "up":
          this.vy = -this.vy_start;
          this.ay = -this.ay_start;
          break;
        case "down":
          this.vy =  this.vy_start;
          this.ay =  this.ay_start;
          break;
      }
    }
  };

Wenn sich der Schläger, aus welchen Gründen auch immer, bereits bewegt, macht sie gar nichts, nur wenn die Geschwindigkeit sowohl in $x$- als auch in $y$-Richtung gleich Null sind, startet sie den Schläger. Abhängig von der vom Benutzer vorgegebenen Bewegungsrichtung (p_direction) werden die Geschwindigkeits- und Beschleunigungs-Start-Werte in die entsprechenden Attribute kopiert. Beachten Sie, dass bei den Richtungen „left“ und „up“ sich jeweils die Vorzeichen gegenüber den Richtungen „right“ und „down“ ändern.

Jetzt fehlt im Modell nur noch die Methode „move“. Diese gibt es im Gegensatz zu den vorherigen beiden Methoden auch für Ball-Objekte.

Allerdings ist folgende Methode etwas allgemeiner:

ModelPaddle.prototype.move =
  function(p_f)
  { this.x  += this.vx/p_f;
    this.y  += this.vy/p_f;

    this.vx += this.ax/p_f;
    this.vy += this.ay/p_f;
  };

Hier wird bei jedem Aufruf nicht nur die Position verändert, sondern auch die Geschwindigkeit. Die Positionsänderung erfolgt anhängig von der aktuellen Geschwindigkeit und die Geschwindigkeitsänderung erfolgt abhängig von der aktuellen Beschleunigung. Wenn in der Initialisierungsdatei „init.json“ ein konstanter Beschleunigungswert vorgegeben ist, wird dadurch erreicht, dass der Schläger immer schneller wird, sobald er einmal gestartet wurde.

View des Schlägers

Der View Konstruktor und die zugehörige Draw-Methode unterscheiden sich nur in zwei wesentlichen Punkten:

  1. Anstelle eines Kreises (l_context.arc) muss ein Rechteck gezeichnet (l_context.rect) gezeichnet werden.
  2. Der Aufhängepunkt eines Rechtecks ist im Canvas-2D-Kontext die linke obere Ecke des Rechtecks ohne Rand. Der Aufhängepunkt eines Kreise ist dagegen dessen Mittelpunkt. Das muss bei Platzieren des Mini-Canvas, der die visuelle Darstellung des Paddels enthält, auf die Bühne berücksichtigt werden. Das heißt, die Draw-Methoden der beiden Objekte unterscheiden sich in dieser Hinsicht.

Der Konstruktor:

function ViewPaddle(p_model_paddle, p_init_view, p_document)
{
  this.modelPaddle = p_model_paddle;
  this.color       = p_init_view.color;
  this.borderWidth = p_init_view.borderWidth;
  this.borderColor = p_init_view.borderColor;

  // Define a local canvas containing the view of the paddle.
  var l_canvas  = this.v_canvas = p_document.createElement("canvas"),
      l_context = l_canvas.getContext("2d");

  l_canvas.width  = p_model_paddle.width  + 2*this.borderWidth + 2;
  l_canvas.height = p_model_paddle.height + 2*this.borderWidth + 2;

  l_context.beginPath();
  l_context.rect(this.borderWidth,     this.borderWidth,
                 p_model_paddle.width, p_model_paddle.height
                );
  l_context.fillStyle   = this.color;
  l_context.fill();     // Fill the inner area of the ball with its color.
  if (this.borderWidth > 0)
  {
    l_context.lineWidth = this.borderWidth;
    l_context.strokeStyle = this.borderColor;
    l_context.stroke(); // Draw the border.
  }
}

Die Draw-Methode:

ViewPaddle.prototype.draw =
  function(p_context)
  {
    p_context
      .drawImage(this.v_canvas,
                 this.modelPaddle.x-this.borderWidth,
                 this.modelPaddle.y-this.borderWidth
                );
  };

Das Spiel

Um Ihre View zu testen, müssen Sie ein Paddle, genauer gesagt: ein Paddle-Model und eine Paddle-View, in Ihr MiniPong-Spiel (MiniPong.js) einfügen. Das Model-Objekt müssen Sie ebenso wie das Ball-Objekt regelmäßig aktualisieren und das neue View-Objekt müssen Sie regelmäßig auf die Bühne zeichnen.

Der Konstruktor sieht nach diesen Erweiterungen folgendermaßen aus:

    function MiniPong(p_init, p_context, p_document)
    {
      var l_model_ball = new ModelBall(p_init.model.ball),
          l_view_ball  = new ViewBall(l_model_ball, p_init.view.ball, p_document),

          l_model_paddle = new ModelPaddle(p_init.model.paddle),
          l_view_paddle  = new ViewPaddle(l_model_paddle, p_init.view.paddle, p_document),

          l_canvas     = p_init.canvas,
          l_f          = p_init.physics.simulationFrequency;

      this.updateModel =
        function()
        {
         // model update
          l_model_paddle.move(l_f);
          l_model_ball.move(l_f);

          // a posteriori collision detection and handling 
          collision(l_canvas, l_model_ball);
        };

      this.updateView =
        function()
        {
          // clear canvas
          p_context.clearRect(0, 0, l_canvas.width, l_canvas.height);

          // draw the paddle and the ball
          l_view_paddle.draw(p_context);
          l_view_ball.draw(p_context);
        };
    }

TO BE DONE

Als Übungsaufgabe sollten Sie die Start-Methode so erweitern, dass auch die vertikalen Bewegungen „rauf“ und „runter“ unterstützt werden. Dies ist insbesondere für eine Implementierung des Spiels „Pong“ wichtig, da dort die beiden Schläger traditionell vertikal bewegt werden. Im Breakout-Spiel „Bolo“ von Atari, kann der Benutzer den Schläger sogar mit allen vier Pfeiltasten steuern und in manchen Situation zwischen horizontalen und vertikalem Layout wechseln. Das ist für MiniPong und Pong allerdings unnötig.

Erweiterung

Erweitern Sie die Anwendung so, dass Sie zwei Schläger unabhängig voneinander links und rechts am Bühnenrand bewegen können.

Musterlösung: index_v.html (WK_MiniPong03a (SVN))
Die Steuerung erfolgt mit den Pfeiltasten. Das zweite Paddle wird mit den Tasten „w“ und „s“ gesteuert.

Quellen

  1. Braun (2011): Herbert Braun; Webanimationen mit Canvas; in: c't Webdesign; Band: 2011; Seite(n): 44–48; Verlag: Heise Zeitschriften Verlag; Adresse: Hannover; 2011; Quellengüte: 5 (Artikel)
  2. Kowarschick (MMProg): Wolfgang Kowarschick; Vorlesung „Multimedia-Programmierung“; Hochschule: Hochschule Augsburg; Adresse: Augsburg; Web-Link; 2018; Quellengüte: 3 (Vorlesung)