HTML5-Tutorium: Canvas: MiniPong 03: Unterschied zwischen den Versionen

aus GlossarWiki, der Glossar-Datenbank der Fachhochschule Augsburg
Zeile 441: Zeile 441:
controlKeyboard(p_window, p_init.control.player, l_model_paddle);
controlKeyboard(p_window, p_init.control.player, l_model_paddle);
</source>
</source>
===Verbesserung der kollisionsbehandlung===
<source lang="javascript">
function collisionPaddleCanvas(p_paddle, p_canvas)
    {
      // If the paddle collides with the left wall of the canvas, stop it.
      if (p_paddle.vx < 0 && p_paddle.x <= 0)
      {
        p_paddle.x = 0; // Move the paddle back to the stage.
        p_paddle.stop();
      }
      // If the paddle collides with the right wall of the canvas, stop it.
      if (p_paddle.vx > 0 && p_paddle.x + p_paddle.width >= p_canvas.width)
      {
        p_paddle.x = p_canvas.width - p_paddle.width; // Move the paddle back to the stage.
        p_paddle.stop();
      }
      // If the paddle collides with the top wall of the canvas, stop it.
      if (p_paddle.vy < 0 && p_paddle.y <= 0)
      {
        p_paddle.y = 0; // Move the paddle back to the stage.
        p_paddle.stop();
      }
      // If the paddle collides with the bottom wall of the canvas, stop it.
      if (p_paddle.vy > 0 && p_paddle.y + p_paddle.height >= p_canvas.height)
      {
        p_paddle.y = p_canvas.height - p_paddle.height; // Move the paddle back to the stage.
        p_paddle.stop();
      }
    }
</source>
<source lang="javascript">
function collisionBallCanvas(p_ball, p_canvas)
    {
      // If the ball collides with the left or the right wall of the canvas
      // mirror its x-velocity.
      if (p_ball.x <= p_ball.r)
      {
        p_ball.x += (p_ball.r - p_ball.x); // Move the ball back to the stage.
        p_ball.vx = -p_ball.vx;
      }
      if (p_ball.x >= p_canvas.width - p_ball.r)
      {
        p_ball.x -=  (p_ball.r - p_canvas.width + p_ball.x); // Move the ball back to the stage.
        p_ball.vx = -p_ball.vx;
      }
      // If the ball collides with the top or the bottom wall of the canvas
      // mirror its y-velocity.
      if (p_ball.y <= p_ball.r)
      {
        p_ball.y += (p_ball.r - p_ball.y); // Move the ball back to the stage.
        p_ball.vy = -p_ball.vy;
      }
      if (p_ball.y >= p_canvas.height - p_ball.r)
      {
        p_ball.y -= (p_ball.r - p_canvas.height + p_ball.y); // Move the ball back to the stage.
        p_ball.vy = -p_ball.vy;
      }
    }
</source>


==Erweiterung==
==Erweiterung==

Version vom 10. November 2016, 13:11 Uhr

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.

Zunächst einmal müssen Sie die Model- und die View-Klasse des Paddles als Module laden. Der Modulkopf der Datei „minipong.js“ muss entsprechend erweitert werden:

define
( ['app/model/ball', 'app/view/ball', 'app/model/paddle', 'app/view/paddle',
   'app/model/collision'
  ],
  function(ModelBall, ViewBall, ModelPaddle, ViewPaddle, collision)

Der eigentliche Konstruktor sieht mit den zuvor beschriebenen 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);
    };
}

Kollisionsbehandlung

Als nächstes muss die Kollisionsbehandlung erweitert werden. Bisher wird nur die Kollision zwischen Ball und Canvas-Wand erkannt und behandelt. Ebenso muss die Kollision zwischen Schläger und Canvas-Wand berücksichtigt werden. Sobald der Schläger die Wand berührt wird er einfach gestoppt. Das heißt, der Benutzer soll den Schläger nicht von der Spielfläche bewegen können. Allerdings kann er ihn natürlich nach jeder Kollision in die Gegenrichtung bewegen. Es gibt noch eine dritte Art der Kollision: Der Schläger berührt den Ball. Diese Kollisionsart wird erst im vierten Teil des Tutoriums behandelt.

Benennen Sie in der Datei „collision.js“ zunächst die Funktion „collision“ in „collisionBallCanvas“ um. Legen Sie außerdem die zunächst leere Funktion „collisionPaddleCanvas

function collisionPaddleCanvas(p_paddle, p_canvas)
{   
}

Schreiben Sie nun eine neue Funktion „collision“, die die beiden zuvor definierten Hilfsfunktionen der Reihe nach aufruft:

function collision(p_ball, p_paddle, p_canvas)
{
  collisionBallCanvas(p_ball, p_canvas);
  collisionPaddleCanvas(p_paddle, p_canvas);
}

Diese Funktion erwartet im Gegensatz zu vorher nun noch einen drittes Argument. Das heißt, in der Init-Datei muss der Aufruf

collision(l_model_ball, l_canvas);

durch den Aufruf

collision(l_model_ball, l_model_paddle, l_canvas);

ersetzt werden.

Wenn Sie jetzt Ihre Anwendung test, sollte sie wieder laufen, d. h., der Ball sollte sich über die Bühne bewegen und der Schläger sollte gezeichnet werden.

      if (   (p_paddle.vx < 0 && p_paddle.x <= 0)
          || (p_paddle.vx > 0 && p_paddle.x + p_paddle.width >= p_canvas.width)
         )
      { p_paddle.stop(); }

      if (   (p_paddle.vy < 0 && p_paddle.y <= 0)
          || (p_paddle.vy > 0 && p_paddle.y + p_paddle.height >= p_canvas.height)
         )
       { p_paddle.stop(); }

Benutzer-Interaktion

    function controlKeyboard(p_window, p_init_keyboard, p_model_paddle)
    {
      var l_key_left      = p_init_keyboard.left.key,
          l_keycode_left  = p_init_keyboard.left.keyCode,

          l_key_right     = p_init_keyboard.right.key,
          l_keycode_right = p_init_keyboard.right.keyCode;

      function o_start_paddle_moving(p_event)
      {
        if (p_event.key === l_key_left || p_event.keyCode === l_keycode_left)
          p_model_paddle.start("left");
        else if (p_event.key === l_key_right || p_event.keyCode === l_keycode_right)
          p_model_paddle.start("right");
      }

      function o_stop_paddle_moving(p_event)
      {
        // If a key is released that controls the movement of
        // the paddle, stop the movement of the paddle.
        if (p_event.key === l_key_left  || p_event.keyCode === l_keycode_left ||
            p_event.key === l_key_right || p_event.keyCode === l_keycode_right
           )
          p_model_paddle.stop();
      }

      p_window.addEventListener("keydown", o_start_paddle_moving);
      p_window.addEventListener("keyup",   o_stop_paddle_moving);
    }
define
( ['app/model/ball', 'app/view/ball', 'app/model/paddle', 'app/view/paddle',
   'app/model/collision', 'app/control/keyboard'
  ],
  function(ModelBall, ViewBall, ModelPaddle, ViewPaddle, collision, controlKeyboard)
controlKeyboard(p_window, p_init.control.player, l_model_paddle);

Verbesserung der kollisionsbehandlung

function collisionPaddleCanvas(p_paddle, p_canvas)
    {
      // If the paddle collides with the left wall of the canvas, stop it.
      if (p_paddle.vx < 0 && p_paddle.x <= 0)
      {
        p_paddle.x = 0; // Move the paddle back to the stage.
        p_paddle.stop();
      }

      // If the paddle collides with the right wall of the canvas, stop it.
      if (p_paddle.vx > 0 && p_paddle.x + p_paddle.width >= p_canvas.width)
      {
        p_paddle.x = p_canvas.width - p_paddle.width; // Move the paddle back to the stage.
        p_paddle.stop();
      }

      // If the paddle collides with the top wall of the canvas, stop it.
      if (p_paddle.vy < 0 && p_paddle.y <= 0)
      {
        p_paddle.y = 0; // Move the paddle back to the stage.
        p_paddle.stop();
      }

      // If the paddle collides with the bottom wall of the canvas, stop it.
      if (p_paddle.vy > 0 && p_paddle.y + p_paddle.height >= p_canvas.height)
      {
        p_paddle.y = p_canvas.height - p_paddle.height; // Move the paddle back to the stage.
        p_paddle.stop();
      }
    }
function collisionBallCanvas(p_ball, p_canvas)
    {
      // If the ball collides with the left or the right wall of the canvas
      // mirror its x-velocity.
      if (p_ball.x <= p_ball.r)
      {
        p_ball.x += (p_ball.r - p_ball.x); // Move the ball back to the stage.
        p_ball.vx = -p_ball.vx;
      }
      if (p_ball.x >= p_canvas.width - p_ball.r)
      {
        p_ball.x -=  (p_ball.r - p_canvas.width + p_ball.x); // Move the ball back to the stage.
        p_ball.vx = -p_ball.vx;
      }

      // If the ball collides with the top or the bottom wall of the canvas
      // mirror its y-velocity.
      if (p_ball.y <= p_ball.r)
      {
        p_ball.y += (p_ball.r - p_ball.y); // Move the ball back to the stage.
        p_ball.vy = -p_ball.vy;
      }
      if (p_ball.y >= p_canvas.height - p_ball.r)
      {
        p_ball.y -= (p_ball.r - p_canvas.height + p_ball.y); // Move the ball back to the stage.
        p_ball.vy = -p_ball.vy;
      }
    }


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)