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

aus GlossarWiki, der Glossar-Datenbank der Fachhochschule Augsburg
Zeile 89: Zeile 89:
rechts bewegen kann.
rechts bewegen kann.


===Modell des Schläger===
===Modell des Schlägers===


Das Model des Schlägers ist ähnlich aufgebaut, wie das Modell des Balls.  
Das Model des Schlägers ist ähnlich aufgebaut, wie das Modell des Balls.  
Zeile 240: Zeile 240:
ein konstanter Beschleunigungswert vorgegeben ist, wird dadurch erreicht, dass der Schläger
ein konstanter Beschleunigungswert vorgegeben ist, wird dadurch erreicht, dass der Schläger
immer schneller wird, sobald er einmal gestartet wurde.  
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:
# Anstelle eines Kreises (<code>l_context.arc</code>)  muss ein Rechteck gezeichnet  (<code>l_context.rect</code>) gezeichnet werden.
# 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:
<source lang="javascript">
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.
  }
}
</source>
Die Draw-Methode:
<source lang="javascript">
ViewPaddle.prototype.draw =
  function(p_context)
  {
    p_context
      .drawImage(this.v_canvas,
                this.modelPaddle.x-this.borderWidth,
                this.modelPaddle.y-this.borderWidth
                );
  };
</source>
{{TBD}}
{{TBD}}



Version vom 10. November 2016, 12:24 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
                );
  };

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.

main.js

Die JavaScript-Datei main.js ist prinzipiell genauso aufgebaut, wie main.js in Teil 1 des Tutoriums. Sie enthält ein Schläger-Objekt (Paddle), speichert den 2D-Kontext und den Animationstimer in zwei weiteren Variablen und initialisiert die Anwendung, nachdem die HTML-Seite vollständig geladen wurde, mit Hilfe einer >code>init-Funktion. Auch die Observer-Funktion o_redraw gibt es wieder.

Die Attribute des Schlägers unterscheiden sich etwas von den Attributen des Balls aus dem ersten Teil des Tutoriums. Der Schläger kann sich nur in x-Richtung (hin- und her-) bewegen, daher wird der Wert vy (= Geschwindigkeit in y-Richtung) nicht gespeichert. Der Schläger bewegt sich zunächst nicht. Er fängt erst mit der Bewegung an, sobald der Spieler eine bestimmte Taste betätigt. Die Start-Geschwindigkeit wird im Attribut vx_start gespeichert. Je länger der Spieler die Taste betätigt, desto schneller bewegt sich der Schläger. Der zugehörige Beschleunigungsfaktor wird im Attribut ax gespeichert.

"use strict";

var g_context, // the 2d context of the canvas
    g_timer,   // the "physics engine" timer
    g_paddle = // the paddle
    { width:  PADDLE_WIDTH,
      height: PADDLE_HEIGHT,
      x:      PADDLE_X,
      y:      PADDLE_Y,
      vx:     0,           // initially the paddle is not moving 
      ax:     PADDLE_AX, 

      // Moves the paddle in direction (vx, 0) and accelerates it;
      // the step size depends on FPS.
      move:
        function()
        { this.x  += this.vx/FPS;
          this.vx += this.ax/FPS * (this.vx > 0 ? 1 : -1);
        },

      // Draws the paddle at its current position onto a 2d context.
      draw:
        function(p_context)
        { p_context.beginPath();
          p_context.fillStyle = PADDLE_COLOR;
          p_context.rect (this.x, this.y, this.width, this.height);
          p_context.fill();
        }
    };

// An event observer: 
// It is called every 1000/FPS milliseconds.
function o_redraw() 
{ // Move the paddle, if possible (collision detection and response)
  if (   (g_paddle.vx < 0 && g_paddle.x > 0)
      || (g_paddle.vx > 0 && g_paddle.x + g_paddle.width < CANVAS_WIDTH)
     )
  { g_paddle.move(); }

  // Clear and redraw the canvas.
  g_context.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
  g_paddle.draw(g_context);
}

// An event observer: 
// It is called whenever a "start paddle moving event" is signaled.
function o_start_paddle_moving(p_event)
{ if (g_paddle.vx == 0)  // react only if the paddle is not already moving
  { if (p_event.keyCode == KEY_LEFT)
    { g_paddle.vx = -PADDLE_VX_START; }
    else if (p_event.keyCode == KEY_RIGHT)
    { g_paddle.vx = +PADDLE_VX_START; }
  }
}

// An event observer: 
// It is called whenever a "stop paddle moving event" is signaled.
function o_stop_paddle_moving(p_event)
{ g_paddle.vx = 0; }

In der Datei main.js müssen noch zwei weitere Observer-Funktionen definiert werden: o_start_paddle_moving und o_stop_paddle_moving.

In der zuvor definierten init-Funktion wurde festgelegt, dass eine der Funktionen jedesmal dann ausgeführt wird, wenn ein spezielles Ereignis eintritt:

  • Spieler drückt eine Taste: o_start_paddle_moving wird aufgerufen.
  • Spieler lässt eine Taste los: o_stop_paddle_moving wird aufgerufen.

Im ersten Fall wird – sofern sich der Schläger noch nicht bewegt – nachgesehen, welche Taste der Spieler gedrückt hat. Falls es eine der beiden Steuerungstasten des Schläger ist, wird der Schläger in die richtige Richtung in Bewegung gesetzt.

Im zweiten Fall wird der Schläger einfach gestoppt, egal ob und in welche Richtung er sich gerade bewegt. Diese Art der Steuerung ist zugegebenermaßen etwas grob. Man kann beispielsweise den Schläger zunächst mit einem Tastendruck starten kann und dann eine beliebige zweite Taste drücken (ohne die erste Taste loszulassen). Wenn man die zweite Taste loslässt, stoppt die Schlägerbewegung, obwohl diese eigentlich nichts mit der Schlägersteuerung zu tun hat. Wenn Sie diese Verhaltensweise stört, können Sie den Code der Stopp-Funktion gerne etwas weniger „grob“ formulieren.

// An event observer: 
// It is called whenever a "start paddle moving event" is signaled.
function o_start_paddle_moving(p_event)
{ if (g_paddle.vx == 0)  // react only if the paddle is not already moving
  { if (p_event.keyCode == KEY_LEFT)
    { g_paddle.vx = -PADDLE_VX_START; }
    else if (p_event.keyCode == KEY_RIGHT)
    { g_paddle.vx = +PADDLE_VX_START; }
  }
}

// An event observer: 
// It is called whenever a "stop paddle moving event" is signaled.
function o_stop_paddle_moving(p_event)
{ g_paddle.vx = 0; }

Zu guter letzt muss die Anwendung wie üblich initialisiert werden:

// Has to be called after the HTML page has been loaded.
function f_init() 
{ var l_canvas = document.getElementById("d_canvas");

  // Initialize the canvas.
  l_canvas.width  = CANVAS_WIDTH;
  l_canvas.height = CANVAS_HEIGHT;
  g_context       = l_canvas.getContext("2d");
  
  // React to key down and key up events.
  document.onkeydown = o_start_paddle_moving;
  document.onkeyup   = o_stop_paddle_moving;

  // Start the timer for redrawing the canvas every 1000/FPS milliseconds.
  g_timer = window.setInterval(o_redraw, 1000/FPS);
}

// Execute the init function after the HTML page has been loaded.
window.onload = f_init;

Anwendung testen und sichern

Testen Sie Ihre Anwendung in gewohnter Weise.

Vergessen Sie nicht, sie im SVN-Repository zu sichern.

Erweiterung

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

Das Objektmodell von MiniPong 02a

Beachten Sie wieder das Programmierprinzip „Don't repeat yourself“ (Dry).

Musterlösung: Minipong 2a (SVN-Repository)

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)

Fortsetzung des Tutoriums

Sie sollten nun Teil 3 des Tutoriums bearbeiten.