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

aus GlossarWiki, der Glossar-Datenbank der Fachhochschule Augsburg
Keine Bearbeitungszusammenfassung
 
(32 dazwischenliegende Versionen von 2 Benutzern werden nicht angezeigt)
Zeile 1: Zeile 1:
{{HTML5-Tutorium:Canvas:MiniPong:Menü}}
{{HTML5-Tutorium:Canvas:MiniPong:Menü}}
'''Musterlösung''': <code>[http://glossar.hs-augsburg.de/beispiel/tutorium/html5_canvas/minipong/html5_canvas_minipong_04/WebContent/index.html Minipong 4]</code>
'''Musterlösung''': <code>[https://glossar.hs-augsburg.de/beispiel/tutorium/es5/minipong/WK_MiniPong05/web/index.html index.html]</code>
([http://glossar.hs-augsburg.de/beispiel/tutorium/html5_canvas/minipong/html5_canvas_minipong_04 SVN-Repository])
([https://glossar.hs-augsburg.de/beispiel/tutorium/es5/minipong/WK_MiniPong05/ WK_MiniPong05 (SVN)])
=Ziel: Simulation von Klassen in JavaScript=
Im vierten Teil des Tutoriums wird beschrieben, wie man [[Klasse]]n in JavaScript nachbilden kann.


Vier Klassen werden implementiert: <code>Ball</code>, <code>Paddle</code>, <code>Collision</code> und <code>Main</code>.
==Ziel: Das fertige Spiel „MiniPong“ verbessern==
Es wird jeweils ein Objekt pro Klasse erstellt.
Im fünften Teil des Tutoriums wird beschrieben, wie die funktionsfähige Version von MiniPong verbessert und erweitert werden kann.
 
[[Medium:MiniPong04Canvas.png|miniatur|ohne|709px|Das Datenmodell von MiniPong 04]]
 
=Anwendung „<code>MiniPongCanvas04</code>“=
 
==Neues Projekt anlegen==
 
Legen Sie ein neues „Statisches Web-Projekt“ mit dem Namen <code>MiniPongCanvas04</code> an.
 
Speichern Sie dieses Projekt wie üblich in Ihrem Repository.
 
==Dateien erstellen==
 
Kopieren Sie die Dateien <code>index.html</code>, <code>css/main.css</code> und <code>js/CONSTANT.js</code> von [[HTML5-Tutorium: Canvas: MiniPong 03|Teil 3]] des Tutoriums,
passen Sie den Projekttitel in der Datei <code>index.html</code> an.
 
Jede Klasse soll in einer eigenen Datei definiert werden, wie dies auch bei anderen Programmiersprachen wie z.B. Java üblich ist.
Erstellen Sie zu diesem Zweck zunächst drei leere Dateien <code>js/ball.js</code>, <code>js/paddle.js</code> sowie <code>js/collision.js</code>
und fügen Sie anschließend folgende drei Zeilen in die Datei <code>index.html</code> ein (vor der Zeile, in der <code>js/main.js</code> geladen wird):
 
<source lang="javascript">
<script type="text/javascript" src="js/ball.js"    ></script>
<script type="text/javascript" src="js/paddle.js"  ></script>
<script type="text/javascript" src="js/collision.js"></script>
</source>
 
'''Anmerkung:''' Um den Kommunikations-Overhead zwischen Client (Browser) und Server möglichst gering zu halten,
sollte eine HTML-Seite möglichst wenige weitere Dokumente (Bilder, CSS-Dateien, JavaScript-Dateien etc.) nachladen.
Die Definition weiterer JavaSCript-Dateien stellt hier aber kein Problem dar, wenn – sobald die Anwendung fertiggestellt und ausgiebig getestet wurde –
alle JavaScript-Dateien komprimiert und in eine einzige Datei (<code>js/all.min.js</code>) eingefügt werden.
Natürlich darf in der Datei <code>index.html</code> anschließend auch nur noch diese eine JavaScript-Datei geladen werden.
 
==Die Klasse <code>Ball</code>==
 
Die Definition der Klasse <code>Ball</code> wird in JavaScript durch zwei Objekte realisiert oder – besser gesagt – simuliert:
Eine Konstruktor-Funktion (genauer: ein Konstruktor-Funktion-Objekt) sowie das zugehörige <code>prototype</code>-Objekt.
 
Die Konstruktor-Funktion <code>Ball</code> erstellt neue Ball-Objekte und initialisiert diese,
indem sie im jedem neu erstellten Objekt (<code>this</code>) Attribute erzeugt und initialisiert.
(Ein Attribut wird automatisch erzeugt, sobald ihm ein Wert zugewiesen wird.)
 
===Konstruktor===
<source lang="javascript">
/**
*  Creates an instance of <code>Ball</code>.
*
*  @constructor
*  @this {Ball}
*  @param {CanvasRenderingContext2D} p_context
*        The 2d context of the canvas upon which the ball is to be drawed.
*  @param {number} p_r
*        The radius of the ball.
*  @param {number} [p_x_start = BALL_X]
*        The starting position of the ball in x-direction.
*  @param {number} [p_y_start = BALL_Y]   
*        The starting position of the ball in y-direction.
*  @param {number} [p_vx_start_min = BALL_VX_MIN]
*        The minimal starting velocity of the ball in x-direction.
*  @param {number} [p_vx_start_max = BALL_VX_MAX]
*        The maximal starting velocity of the ball in x-direction.
*  @param {number} [p_vy_start_min = BALL_VY_MIN]
*        The minimal starting velocity of the ball in y-direction.
*  @param {number} [p_vy_start_max = BALL_VY_MAX}
*        The maximal starting velocity of the ball in y-direction.
*/
function Ball(p_context,
              p_r, p_x_start, p_y_start,
              p_vx_start_min, p_vx_start_max,
              p_vy_start_min, p_vy_start_max
            )
{this.r = p_r || BALL_RADIUS;
 
  this.v_x_start        = p_x_start      || BALL_X;
  this.v_y_start        = p_y_start      || BALL_Y;
  this.v_vx_start_min  = p_vx_start_min || BALL_VX_MIN;
  this.v_vx_start_max  = p_vx_start_max || BALL_VX_MAX;
  this.v_vy_start_min  = p_vy_start_min || BALL_VY_MIN;
  this.v_vy_start_max  = p_vy_start_max || BALL_VY_MAX;
 
  this.v_vx_start_delta = this.v_vx_start_max - this.v_vx_start_min;
  this.v_vy_start_delta = this.v_vy_start_max - this.v_vy_start_min;
 
  this.v_context  = p_context;
 
  this.reset();
};
</source>
 
Die <code>reset</code>-Methode nimmt weitere Initialisierungen vor.
Da diese Initialisierungen bei jedem Spielstart erneut ausgeführt werden müssen,
wurden sie in eine Methode ausgelagert.
 
Man beachte, dass der dem Konstruktor vorangestellte Kommentar [[JSDoc]]-konform ist.
Dies ermöglicht es, mit einem geeigneten JSDoc-Tool eine HTML-[[API]]-Dokumentation automatisch zu erstellen.
 
===Methoden===
 
Der Konstruktor weist jedem neu erstellten Objekt ein weiteres Attribut automatisch zu: <code>prototype</code>.
Dieses Attribut enthält einen Verweis auf das <code>prototype</code>-Objekt des zugehörigen Konstruktors.
JavaScript sucht bei einem Methodenaufruf <code>obj.m()</code> die Definition der Methode <code>m</code>
zunächst im Objekt <code>obj</code> selbst. Sollte dort keine Definition vorhanden sein, so
sucht JavaScript als nächstes in <code>obj.prototype</code>, dann in
<code>obj.prototype.prototype</code> usw. Die Suche endet sobald entweder eine Definition gefunden oder
kein weiteres <code>prototype</code>-Objekt mehr existiert. Im letzteren Fall meldet JavaScript einen Fehler.
 
Das heißt, in JavaScript werden Methoden üblicherweise im Prototyp-Objekt des Konstruktors definiert.
 
(Anmerkung: Auch hier werden wieder JSDoc-konforme Kommentare benutzt.)
 
<source lang="javascript">
Ball.prototype =
{ //////////////////////////////////////////////////////////////////////////////
  // data
  //////////////////////////////////////////////////////////////////////////////
 
  /** The radius of the ball. */ 
  get r()    { return this.v_r; },
  set r(p_r) { this.v_r = p_r;  },
 
  /** The x-position of the ball. */
  get x()    { return this.v_x; },
  set x(p_x) { this.v_x = p_x;  },
 
  /** The y-position of the ball. */
  get y()    { return this.v_y; },
  set y(p_y) { this.v_y = p_y;  },
 
  /** The vx-velocity of the ball. */
  get vx()    { return this.v_vx; },
  set vx(p_vx) { this.v_vx = p_vx; },
 
  /** The vy-velocity of the ball. */
  get vy()    { return this.v_vy; },
  set vy(p_vy) { this.v_vy = p_vy; },
 
  //////////////////////////////////////////////////////////////////////////////
  // logic
  //////////////////////////////////////////////////////////////////////////////
 
  /**
  * Resets the ball:
  *  Moves the ball to its starting position
  *  and computes randomly a starting velocity vector.
  */
  reset:
    function()
    { this.x  = this.v_x_start;
      this.y  = this.v_y_start;
      this.vx = (Math.random()<0.5?1:-1)
                * (this.v_vx_start_min+Math.random()*this.v_vx_start_delta);
      this.vy = (this.v_vy_start_min+Math.random()*this.v_vy_start_delta);
    },
 
  /** Moves the ball in direction (vx,vy); the step size depends on FPS. */
  move:
    function()
    { this.x += this.vx/FPS;
      this.y += this.vy/FPS;
    },
 
  //////////////////////////////////////////////////////////////////////////////
  // view
  //////////////////////////////////////////////////////////////////////////////
 
  /** Draws the ball at its current position onto a 2d context. */
  draw:
    function()
    { this.v_context.beginPath();
      this.v_context.arc(this.x, this.y, this.r, 0, 2*Math.PI, true);
      this.v_context.lineWidth = BALL_BORDER_WIDTH;
      this.v_context.lineStyle = BALL_BORDER_COLOR;
      this.v_context.fillStyle = BALL_COLOR;
      this.v_context.stroke();
      this.v_context.fill();
    },
 
  //////////////////////////////////////////////////////////////////////////////
  // end of prototype
  //////////////////////////////////////////////////////////////////////////////
};
</source>
 
Beachten Sie, dass die [[Getter-Methode|Getter]]- und [[Setter-Methode]]n für die
Attribute <code>r</code>, <code>x</code>, <code>y</code>, <code>vx</code>, <code>vy</code>
EcmaScript-5-konform definiert wurden (EcmaScript 5 wird von HTML5-fähigen Brwosern unterstützt).
Dies hat den Vorteil, dass man auf diese Attribute nicht mit der Methoden-Aufruf-Syntax,
sondern mit der üblichen Attribut-Syntax zugreifen kann:
 
<source lang="JavaScript">
var my_ball = new Ball();
 
my_ball.r = 20;        // Aufruf der Setter-Methode
console.log(my_ball.r); // Aufruf der Getter-Methode
</source>
 
In EcmaScript 3 (HTML 4 oder früher)
gibt es diese Art der Getter- und Setter-Methoden nicht. Hier muss man auf
den in Java üblichen Trick zurückgreifen und ersatzweise zwei Methoden definieren.
 
<source lang="JavaScript">
/** The radius of the ball. */ 
getR: function()    { return this.v_r; },
setR: function(p_r) { this.v_r = p_r;  },
 
...
</source>
 
In diesem Fall erfolgen die Attribut-Zugriffe mittels Methodenaufrufen:
 
<source lang="JavaScript">
var my_ball = new Ball();
 
my_ball.setR(20);            // Aufruf der Setter-Methode
console.log(my_ball.getR()); // Aufruf der Getter-Methode
</source>
 
Die Verwendung von echten Getter- und Setter-Methoden erhöht die
[[Programmierprinzipien#Stetigkeit.2C_Continuity|Stetigkeit]] des Codes,
da sich der Zugriff auf öffentliche Zustandsvariablen syntaktisch nicht vom
Zugriff auf echte Getter- und Setter-Methoden unterscheidet. Das heißt,
die Implementierung der Klasse kann jederzeit die Art der Attribut-Definition ändern,
ohne dass das Auswirkungen auf die Zugriff-Syntax von zugehörigen Objekten hätte.<ref>vgl. {{Quelle|Meyer (1997)}}</ref> 
 
Beispielsweise könnten in der obigen Definition der Klasse <code>Ball</code>
die Getter- und Setter-Methoden der Attribute <code>x</code> und <code>y</code>
(ebenso wie alle anderen Getter- und Setter-Methoden) einfach aus dem Prototype-Objekt gelöscht werden.
Am Verhalten der Klasse <code>Ball</code> würde das nichts ändern, da die Methoden
<code>reset</code> und <code>move</code> nun direkt auf die Zustandsvariablen
<code>this.x</code> und <code>this.y</code> zugreifen würden. Der zugehörige
Code wäre sogar etwas effizienter.
 
Kommentieren Sie in der Musterlösung oder in Ihrer finalen Lösung
doch einfach mal ein paar Getter- und Setter-Methoden aus und starten Sie die Anwendung erneut.
Die Anwendung sollte immer noch fehlerfrei funktioneiren.
Anschließend sollten Sie die Methoden wieder aktivieren und testweise einmal einen Log-Befehl einfügen:
 
<source lang="javascript">
set x(p_x) { this.v_x = p_x; console.log("Setter 'x', param: " + p_x); },
</source>
 
Wenn Sie jetzt das die Anwendung ausführen, wird jede Änderung der x-Position des Balls
im Konsolfenster ausgegeben. Ohne echte Setter-Methode hätten Sie diesen Logging-Befehl
an jede Stelle, an der die Ballposition geändert wird, einfügen mussen. Oder Sie hätten
an jeder dieser Stellen <code>...ball.x = ...</code> durch <code>...ball.setX(...)</code>
ersetzen müssen.
 
==Die Klasse <code>Paddle</code>==
 
Die Klasse <code>Paddle</code> wird analog zur Klasse <code>Ball</code> definiert.
Die benötigten Attribute und Methoden findet man im obigen Klassendiagramm.
 
Die Methoden-Definitionen von <code>reset</code>, <code>move</code> und <code>draw</code> werden
(wie bei der Klasse <code>Ball</code> aus) der Lösung des [[HTML5-Tutorium: Canvas: MiniPong 03|dritten Teils des Tutoriums]] übernommen.
Bei der Definition der Methode <code>reset</code> sollte analog zur Klasse <code>Ball</code>
der Zugriff auf globale Konstanten (<code>PADDLE_X</code> und <code>PADDLE_Y</code>)
durch den Zugriff auf lokale Zustandsvariablen (<code>this.v_x_start</code> und <code>this.v_y_start</code>)
ersetzt werden. Diese beiden Variablen sollten (wie in der Klasse <code>Ball</code>) von Konstruktor initialisiert werden.
 
Zusätzlich werden drei weitere Methoden benötigt:
 
<source lang="javascript">
/** Makes the paddle move left (if it is currently not moving). */ 
startLeft:  function(){ if (this.vx == 0) this.vx = -this.v_vx_start; },
 
/** Makes the paddle move right (if it is currently not moving). */ 
startRight: function(){ if (this.vx == 0) this.vx = +this.v_vx_start; },
 
/** Makes the paddle stop moving. */ 
stop:      function(){ this.vx = 0; },
</source>
 
Die Attribute werden – bis auf vier Ausnahmen – genauso definiert wie in der Klasse <code>Ball</code>:
Entweder gar nicht oder mittels echter Setter- und Getter-Methoden.
 
Vier Atribute wurden allerdings im Diagramm als nur lesbar (read only, <code>frozen</code>)
deklariert: <code>left</code>, <code>right</code>, <code>top</code> und <code>bottom</code>.
Diese Attribute dürfen also nicht geändert, sondern nur gelesen werden,
da die Werte aus anderen Attributen berechnet werden. Daher können die Werte auch nicht
in Zustandsvariablen gespeichert werden. Allerdings kann man Read-only-Attribute
mit Hilfe echter Getter-Methoden recht elegant realisieren:
 
<source lang="javascript">
/** The left side of the paddle (read only). */
get left() { return this.x; },
 
/** The right side of the paddle (read only). */
get right() { return this.x + this.width; },
 
/** The top side of the paddle (read only). */
get top() { return this.y;  },
 
/** The bottom side of the paddle (read only). */
get bottom() { return this.y + this.height; },
</source>
 
Auf diese Attribute kann ebenfalls mit der üblichen Attribut-Syntax zugegriffen werden:
 
<source lang="javascript">
var my_paddle = new Paddle();
 
console.log(my_paddle.left); // Aufruf der Getter-Methode
</source>
 
Da keine Setter-Methoden existieren, können diese Attribute nicht geändert werden.
Falls man seinen JavaScript-Code mittels <code>"use strict";</code>  als „strikt“ dekalriert hat,
resultiert folgende Anweisung in einer Fehlermeldung:
 
<source lang="javascript">
my_paddle.left = 30; // Aufruf der nicht vorhandenen Setter-Methode
</source>
 
==Die Klasse <code>Collision</code>==
 
Ein Objekt der Klass <code>Collision</code> ist für das Erkennen und Behandlen von Kollisionen zuständig.
 
Dem Konstruktor müssen alle möglich Kollisionspartner übergeben werden: Wände, Ball und Schläger.
 
<source lang="javascript">
/**
*  Creates an instance of <code>Collision</code>.
*  The main method <code>handleCollision</code> detects collisions
*  between the ball and a wall, between the paddle and a wall, and
*  between the ball and the paddle.
*
*  @constructor
*  @this {Collision}
*  @param {number} p_wall_left  The x-coordinate of the left wall.
*  @param {number} p_wall_right  The x-coordinate of the right wall.
*  @param {number} p_wall_top    The y-coordinate of the top wall.
*  @param {number} p_wall_bottom The y-coordinate of the bottom wall.
*  @param {Ball}  p_ball        The ball of the game.
*  @param {Paddle} p_paddle      Ther paddle of the game.
*/
function Collision(p_wall_left, p_wall_right, p_wall_top, p_wall_bottom,
                  p_ball, p_paddle
                  )
{ this.v_wall_left  = p_wall_left;
  this.v_wall_right  = p_wall_right;
  this.v_wall_top    = p_wall_top;
  this.v_wall_bottom = p_wall_bottom;
 
  this.v_ball  = p_ball;
  this.v_paddle = p_paddle;
};
</source>
 
In der Kollisionsklasse wird nur eine öffentliche Methode definiert: <code>handleCollision</code>.
Diese Methode hat zwei Aufgabe: Kollisionen erkennen und erkannte Kollisionen zu behandeln.
Dabei sind zwei Arten von Behandlung möglich:
# Die Kollisionspartner ändern ihr Verhalten (Richtungswechsel des Balls, Stopp des Schläger).
# Das Spiel reagiert auf die Kollisionen (Erhöhung der Punktezahl, Spielende)
 
Die erste Art der Behandlung nimmt <code>handleCollision</code> selbst vor,
da ihr die Kollisionspartner bekannt sind. Die zweite Art der Kollisionsbehandlung
kann diese Methode dagegen nicht vornehmen. Daher informiert sie den Aufrufer
mittels eines Rückgabewerts (<code>EVENT_SCORE</code>: Erhöhung des Punktekontots, <code>EVENT_EXIT</code>: Spielende)
über das eingetretene Ereignis. Es ist dann die Aufgabe des Aufrufers, entsprechende Maßnahmen
vorzunehmen.
 
<source lang="javascript">
Collision.prototype =
{ //////////////////////////////////////////////////////////////////////////////
  // Methods (Logic)
  //////////////////////////////////////////////////////////////////////////////
 
  /**
  *  Detects collisions between the ball and a wall,
  *  between the paddle and the wall, and between the ball and the paddle.
  *  If a collision is detected, the collision is handled.
  *  <p>
  *    This method is an update method returning an event
  *    name in som cases. Better event handling should be
  *    implemented.
  *  </p>
  *  @return {string} <code>EVENT_EXIT</code> if the ball hits the bottom wall,
  *                  <code>EVENT_SCORE</code> if the ball hits the paddle.
  *                  <code>NULL</code> otherwise
  */ 
  handleCollision:
    function()
    { this.m_collision_ball_wall();
      this.m_collision_paddle_wall();
 
      if (this.m_ball_exit())
        return EVENT_EXIT;
 
      if (this.m_collision_ball_paddle())
        return EVENT_SCORE;
 
      return null;
    },
 
  //////////////////////////////////////////////////////////////////////////////
  // Private Methods (Logic)
  //////////////////////////////////////////////////////////////////////////////
 
  // Tests whether the ball hits the bottom wall. 
  m_ball_exit:
    function()
    { return (this.v_ball.y >= this.v_wall_bottom + this.v_ball.r);
    },
 
  // Tests whether the ball hits another wall. 
  m_collision_ball_wall:
    function()
    { if (  (this.v_ball.x <= this.v_wall_left  + this.v_ball.r)
          || (this.v_ball.x >= this.v_wall_right - this.v_ball.r)
        )
        this.v_ball.vx = -this.v_ball.vx;
 
      if (this.v_ball.y <= this.v_wall_top + this.v_ball.r)
        this.v_ball.vy = -this.v_ball.vy;
    },
 
  // Tests whether the paddle hits the bottom wall and, if so, stops it.
  m_collision_paddle_wall:
    function()
    { if (  (this.v_paddle.left  <= this.v_wall_left  && this.v_paddle.vx < 0)
          || (this.v_paddle.right >= this.v_wall_right && this.v_paddle.vx > 0)
        )
        this.v_paddle.stop();
    },
 
  // Tests whether the paddle hits the paddle.
  m_collision_ball_paddle:
    function()
    { if (  this.v_ball.y + this.v_ball.r    >= this.v_paddle.top
          && this.v_ball.y + this.v_ball.r    <= this.v_paddle.bottom
          && this.v_ball.x + 0.5*this.v_ball.r >= this.v_paddle.left
          && this.v_ball.x - 0.5*this.v_ball.r <= this.v_paddle.right
        )
      { // Resolve penetration.
        if (this.v_ball.vy > 0)        // The ball is moving from top to bottom.
          this.v_ball.y = this.v_paddle.y - this.v_ball.r;
        else                          // The ball is moving from bottom to top.
          this.v_ball.y = this.v_paddle.y + this.v_paddle.height + this.v_ball.r;   
 
        this.v_ball.vy = -this.v_ball.vy;
        this.v_ball.vx += this.v_paddle.friction*this.v_paddle.vx;
        return true;
      };
      return false;
    },
 
};
</source>
 
Man beachten, dass in der obigen Definition vier private Methoden definiert wurden,
die für die jeweils unterschiedlichen Kollisionsszenarien zuständig sind:
* Ball kollidiert mit unterer Wand (⇒ Spielende)
* Ball kollidiert mit einer anderen Wand (⇒ Richtungsänderung)
* Schläger kollidiert mit einer Wand (⇒ Stopp der Schlägerbewegung)
* Ball kollidiert mit Schläger (⇒ Richtungsänderung und Punktgewinn)
 
Diese Aufteilung wäre hier nicht notwendig gewesen, führt aber zu verständlicherem Code
und legt den Grundstein für eine weitergehende Modularisierung der Kollisionsklasse.
 
==Die Datei <code>main.js</code>==
 
Hier könnte zunächst die Datei <code>main.js</code> von [[HTML5-Tutorium: Canvas: MiniPong 03|Teil 3 des Tutoriums]] übernommen und leicht angepasst werden:
 
Die Variablen[[Definition vs. Deklaration|definition]] von <code>g_ball</code> und <code>g_paddle</code> werden durch Variablen[[Definition vs. Deklaration|deklarationen]]
ersetzt:
 
<source lang="javascript">
// ball, paddle, collision
var g_ball;
var g_paddle;
var g_collision;
</source>
 
Die eigentlich Definition der drei Objekte erfolgt nun in der Funktion <code>f_init</code>
'''nachdem''' das Objekt <code>g_context</code> ermittelt wurde:
 
<source lang="javascript">
// Initialize all game objects.
g_ball      = new Ball(g_context);
g_paddle    = new Paddle(g_context);
g_collision = new Collision(0, CANVAS_WIDTH, 0, CANVAS_HEIGHT+2*BALL_RADIUS,
                            g_ball, g_paddle
                          );
</source>
 
'''Anmerkung:''' Die untere Wand wurde außerhalb des Spielfeldrandes gelegt,
damit der Ball ganz von der Bühne verschwunden ist, bevor das Spielende angezeigt wird.
 
Die einzigen weiteren notwendigen Änderungen sind:
 
Die Aufrufe
<source lang="javascript">
g_ball.draw(g_context);
g_paddle.draw(g_context);
</source>
werden durch
<source lang="javascript">
g_ball.draw();
g_paddle.draw();
</source>
ersetzt, da jedes grafische Objekt den 2D-Kontext, auf den es gezeichnet werden soll, kennt (er wurde dem jeweiligen Objekt bei der Konstruktion übergeben).
 
Die Redraw-Methode wird deutlich einfacher, da deren Aufgabe im Wesentlichen vom Kollisionsobjekt übernommen wurden:
 
<source lang="javascript">
function o_redraw()
{ // collision detection
  var l_exit = g_collision.handleCollision();
 
  if (l_exit == EVENT_EXIT)
  { o_stop_game();
    return;
  };
 
  if (l_exit == EVENT_SCORE)
  { // Raise the score and display it.
    g_score++;
    f_info("Punkte: " + g_score);
  };
  // Move the ball and paddle.
  g_ball.move();
  g_paddle.move();
 
  // Redraw the canvas.
  f_draw();
}
</source>
 
Die Implementierungen der Start- und Stopp-Methoden des Paddles können ebenfalls vereinfacht werden,
da dafür im Paddle-Objekt spezielle Methoden zur Verfügung stehen:
 
<source lang="javascript">
// An event observer:
// It is called whenever a "start paddle moving event" is signalled.
function o_start_paddle_moving(p_event)
{ if (p_event.keyCode == KEY_LEFT)
    g_paddle.startLeft();
  else if (p_event.keyCode == KEY_RIGHT)
    g_paddle.startRight();
}
 
// An event observer:
// It is called whenever a "stop paddle moving event" is signalled.
function o_stop_paddle_moving(p_event)
{ g_paddle.stop(); }
</source>
 
==Eine echte <code>Main</code>-Klasse (<code>main.js</code> reloaded)==
 
Die JavaScript-Datei <code>main.js</code> enthält noch viele globale Werte: globale Variablen (<code>g_...</code>),
globale Funktionen (<code>f_...</code>) und globale Observer-Methoden (<code>o_...</code>).
 
Aus Sicht des „[[Programmierprinzipien#Modularit.C3.A4t.2C_Modularity.2C_Teile_und_herrsche.2C_Divide_et_impera|Prinzips der Modularität]]“
ist die Verwendung von globalen Größen extrem schlecht. Auf globale Größen kann jedes Modul ungehindert zugreifen. Daher
kann eine globale Größe nie geändert werden, ohne vorher sämliche Module zu überprüfen, ob und in welcher Weise sie
auf diese Größe zugreifen. Dies ist eine der Lehren, die zahlreiche [[Fortran]]-Programmierer aus der früher üblichen Programmierpraxis,
sehr viele Daten in einem globalen Datenbereich abzulegen, schmerzhaft ziehen mussten.
 
Das heißt, alle globalen Größen der Datei <code>main.js</code> sollten in einem <code>Main</code>-Objekt gekapselt werden.
 
===Konstruktor===
 
Der Konstruktor übernimmt im Wesentlichen die Aufgaben der ehemaligen Init-Funktion <code>f_init</code>.
Anmerkung: Hier wurde bewusst auf [[JSDoc]]-Kommentare verzichtet, das <code>Main</code> eine
„private“ Klasse ist, die nicht in anderen Projekten wiederverwendet wird. Daher ist es nicht sonderlich sinnvoll,
die API dieser Klasse automatisch zu generieren.
 
<source lang="javascript">
// Construct the main object, after the HTML page has been loaded.
window.onload = function(){ new Main(); };
 
// The singleton class Main.
function Main()
{ // Ensure that Main is a singleton class.
  if (Main.c_element != null)
    return Main.c_element;
  Main.c_element = this;
 
  var l_canvas = document.getElementById("d_canvas");
 
  // Initialize the canvas.
  l_canvas.width  = CANVAS_WIDTH;
  l_canvas.height = CANVAS_HEIGHT;
  this.v_context  = l_canvas.getContext("2d");
 
  // Initialize all game objects.
  this.v_ball      = new Ball(this.v_context);
  this.v_paddle    = new Paddle(this.v_context);
  this.v_collision = new Collision
                        (0, CANVAS_WIDTH, 0, CANVAS_HEIGHT+2*BALL_RADIUS,
                          this.v_ball, this.v_paddle
                        );
 
  var self = this; // Needed to overcome static binding problems.
  document.getElementById("d_start_stop").onmousedown =
    function() { self.o_start_stop_game(); };
 
  // Reset the game (i.e. change its state to "waiting for game to be started").
  this.v_state = STATE_STOPPED;
  this.m_init_stopped();
}
</source>
 
Im obigen Datenmodell wurde die Klasse <code>Main</code> als <code>Singleton</code> deklariert. Das heißt, von dieser
Klasse darf es zu jedem Zeitpunkt maximal ein Objekt geben. Die erste <code>if</code>-Anweisung im Konstruktor
sorgt dafür, dass damit tatsächlich nur ein <code>Main</code>-Objekt erzeugt werden kann. Das erste
<code>Main</code>-Objekt wird im Konstruktorobjekt gespeichert. Sofern tatsächlich versucht werden
würde, ein zweites <code>Main</code>-Objekt mit dem Konstruktor zu erzeugen, wird einfach
das schon existente <code>Main</code>-Objekt als Ergebnis zurückgegeben.
 
Eine sehr subtile Änderung ist allerdings hinsichtlich des Start/Stop-Buttons notwendig.
In JavaScript hängt der Wert der Variablen <code>this</code> nicht vom Definitionsort ab, sondern vom Aufrufer der Methode.
Die Variable <code>this</code> ist also - im Gegensatz zu allen anderne JavaScript-Variablen</code>
nicht [[Bindung|lexikalisch]] sondern [[Bindung|dynamisch]] gebunden.
 
Das Problem ist nun, dass die Methode <code>o_start_stop_game</code> nicht jetzt, sondern erst später aufgerufen wird und zwar
vom im HTML-Dokument enthaltenen Button-Objekt (<code>document.getElementById("d_start_stop")</code>)
sobald der Benutzer auf diesen Button klickt. Damit zeigt die Variable <code>this</code> aud dieses Button-Objekt un nicht
auf das <code>Main</code>-Objekt. Da aber die Methode <code>o_start_stop_game</code> mittels <code>this</code> auf
Attribute des <code>Main</code>-Objekts zugreifen will, muss man eine schmutzigen, aber in JavaScript-Code
häufig anzutreffenden Trick zugreifen. Man muss <code>this</code> '''außerhalb''' der Funktion, die das Button-Objekt
später aufruft, in einer lexikalisch gebundenen Variablen, d.h. in irgendeiner anderen Variablen, ablegen.
Überlicherweise heißt diese Variable <code>self</code>.
 
<source lang="javascript">
var self = this; // Needed to overcome static binding problems.
</source>
Innerhalb der Funktion, die das Button-Objekt später aufruft, wird nun <code>self</code> anstelle von <code>this</code>
verwendet.
 
<source lang="javascript">
document.getElementById("d_start_stop").onmousedown =
  function() { self.o_start_stop_game(); };
</source>
 
Da <code>self</code> lexkalisch gebunden ist, tritt das zuvor beschriebene Problem nicht mehr auf. 
Wenn nun innerhalb der Methode <code>o_start_stop_game</code> auf die Variable <code>this</code>
zugegrifen wird, so ist hierin wie gewünscht das <code>Main</code>-Objekt enthalten.
 
'''Warnung:''' Dieses JavaScript-Verhalten kann gerade einem Programmieranfäger sicher stundenlange Fehlersuche bereiten.
 
===Methoden===
 
Die ehemals öffentlichen Funktionen der JavaScript-Datei <code>main.js</code>
werden als Methoden des <code>Main</code>-Objekts realisiert.
 
<source lang="javascript">
Main.prototype =
{ //////////////////////////////////////////////////////////////////////////////
  // logic
  //////////////////////////////////////////////////////////////////////////////
 
  // Has to be called when the game is to be started.
  m_init_started:
    function ()
    { // Reset the score.
      this.v_score = 0;
 
      // Set the button "d_start_stop" to be a stop button (view).
      document.getElementById("d_start_stop").value = "Stopp";
 
      var self = this; // Needed to overcome static binding problems.
 
      // React to key down and key up events.
// The following code does not work due to static binding of "this" :-(((
//    document.onkeydown = this.o_start_paddle_moving;
//    document.onkeyup  = this.o_stop_paddle_moving;
      document.onkeydown =
        function(p_event){ self.o_start_paddle_moving(p_event); };
      document.onkeyup =
        function(p_event){ self.o_stop_paddle_moving(p_event); };
 
      // Start the timer for redrawing the canvas every 1000/FPS seconds.
      this.v_timer =
        window.setInterval(function(){ self.o_redraw(); }, 1000/FPS);
 
      // Show the current score (zero points)
      this.m_info("Punkte: " + this.v_score);
    },
 
  // Has to be called when the game is to be stopped.
  m_init_stopped:
    function ()
    { // Stop the canvas redrawing timer.
      window.clearInterval(this.v_timer);
 
      // Stop reacting to key down and key up events.
      document.onkeydown = null;
      document.onkeyup  = null;
 
      // Set the button "d_start_stop" to be a start button (view).
      document.getElementById("d_start_stop").value = "Start";
 
      // Put the ball and the paddle on their starting positions
      // and then redraw the canvas.
      this.v_ball.reset();
      this.v_paddle.reset();
      this.m_draw();
    },
   
  // An event observer:
  // It is called presses the start/stop button.
  o_start_stop_game:
    function()
    { // Change the current state (this is a little automaton!).
      this.v_state = NEXT_STATE[this.v_state];
 
      // Initialize the new state.
      this["m_init_" + this.v_state]();
/*    // Alternatively:
*    if (this.v_state == STATE_STARTED)
*      this.m_init_started();
*    else if (this.v_state == STATE_STOPPED)
*      this.m_init_stopped();
*/ 
    },
 
  // An event observer:
  // It is called whenever a "start paddle moving event" is signalled.
  o_start_paddle_moving:
    function(p_event)
    { // console.log(this);
      if (p_event.keyCode == KEY_LEFT)
        this.v_paddle.startLeft();
      else if (p_event.keyCode == KEY_RIGHT)
        this.v_paddle.startRight();
    },
 
  // An event observer:
  // It is called whenever a "stop paddle moving event" is signalled.
  o_stop_paddle_moving:
    function(p_event)
    { this.v_paddle.stop();
    },
 
  //////////////////////////////////////////////////////////////////////////////
  // view
  //////////////////////////////////////////////////////////////////////////////
 
  // Clears the canvas and redraws all sprites (ball and paddle).
  m_draw:
    function()
    { this.v_context.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
   
      this.v_ball.draw();
      this.v_paddle.draw();
    },
 
  // Displays information on the HTML page.
  m_info:
    function (p_info)
  { document.getElementById("d_info").firstChild.nodeValue = p_info; },
 
  // An event observer:
  // It is called every 1000/FPS seconds, but only when the game has been started.
  o_redraw:
    function()
    { // collision detection
      var l_exit = this.v_collision.handleCollision();
 
      if (l_exit == EVENT_EXIT)
      { this.o_start_stop_game();
        return;
      };
     
      if (l_exit == EVENT_SCORE)
      { // Raise the score and display it.
        this.v_score++;
        this.m_info("Punkte: " + this.v_score);
      };
     
      // Move the ball and paddle.
      this.v_ball.move();
      this.v_paddle.move();
 
      // Redraw the canvas.
      this.m_draw();
    },
 
  //////////////////////////////////////////////////////////////////////////////
  // end of prototype
  //////////////////////////////////////////////////////////////////////////////
};
</source>
 
'''Beachte:''' Auch in den Methodendefinitionen musste der
„<code>this</code>/<code>self</code>-Hack“ mehrfach angewandt werden.
Man überlege sich in allen Fällen, warum dies notwendig ist.
 
Um das Problem nachzuvollziehen, sollten Sie die Trace-Anweisung
<code>console.log(this);</code> in der Methode <code>o_start_paddle_moving</code>
(in Ihrer Anwendung oder in der Musterlösung) aktivieren.
 
Starten Sie nun die Anwednung im Firefox und beobachten Sie die Text-Ausgabe im Firebug-Konsolen-Fester,
wenn Sie Ihren Schläger mittels Tastatur-Steuerung bewegen.
 
Aktivieren Sie anschließend im Rumpf der Methode <code>m_init_started</code>
die ersten beiden der folgenden Zeilen und kommentieren Sie die nachfolgenden vier Zeilen aus:
 
<source lang="javascript">
//    document.onkeydown = this.o_start_paddle_moving;
//    document.onkeyup  = this.o_stop_paddle_moving;
      document.onkeydown =
        function(p_event){ self.o_start_paddle_moving(p_event); };
      document.onkeyup =
        function(p_event){ self.o_stop_paddle_moving(p_event); };
</source>
 
Aus obigem Code wird also:
<source lang="javascript">
      document.onkeydown = this.o_start_paddle_moving;
      document.onkeyup  = this.o_stop_paddle_moving;
//    document.onkeydown =
//      function(p_event){ self.o_start_paddle_moving(p_event); };
//    document.onkeyup =
//      function(p_event){ self.o_stop_paddle_moving(p_event); };
</source>
 
Starten Sie nun Ihre Anwendung erneut
und versuchen Sie den Schläger mittels Tastensteuerung zu bewegen.
 
Sie werden feststellen, dass die Methode <code>o_start_paddle_moving</code>
nun nicht mehr auf das <code>Main</code>-Objekt zugreift,
sondern auf das <code>index.html</code>-Objekt (welches JavaScript in der globalen Variablen <code>document</code> zur Verfügung stellt).
Die in diesem Objekt enthaltenen Methoden <code>onkeydown</code> und <code>onkeyup</code> werden von JavaScript
aufgerufen, sobald der Benutzer irgendeine Taste auf dem Keyboard drückt bzw. loslässt.
Da im <code>index.html</code>-Objekt das Objekt <code>v_paddle</code> nicht existiert, reagiert <code>MiniPong</code>
nun bei jedem Tastendruck mit einer Fehlermeldung.
 
In der ursprünglichen Version von <code>main.js</code> bestand dieses Problem noch nicht,
da alle Funktionen (wie z.B. <code>o_start_paddle_moving</code>) und Variablen global definiert wurde.
Im globalen Kontext bezieht sich </code>this</code> auch auf das globale Objekt.
 
=Erweiterung=
 
Erweitern Sie die Anwendung wiederum so, dass Sie zwei Schläger unabhängig voneinander
links und rechts am Bühnenrand bewegen können, d.h., dass zwei Spieler gegeneinander spielen können.
Jeder Spieler hat seinen eigenen Score. Der erste Spieler, der 7 Punkte erreicht, gewinnt.
(Vergleiche [[HTML5-Tutorium: Canvas: MiniPong 03#Erweiterung]])
 
{{TBD}}
 
[[Medium:MiniPong04aCanvas.png|miniatur|ohne|945px|Das Objektmodell von MiniPong 04a]]
 
'''Musterlösung''': <code>[http://glossar.hs-augsburg.de/beispiel/tutorium/html5_canvas/minipong/html5_canvas_minipong_04a/WebContent/index.html Minipong 4a]</code>
([http://glossar.hs-augsburg.de/beispiel/tutorium/html5_canvas/minipong/html5_canvas_minipong_04a SVN-Repository])
 
=Verbesserungsmöglichkeiten=
 
Die Code-Qualität von <code>MiniPongCanvas04</code> hat sich gegenüber
<code>[[HTML5-Tutorium: Canvas: MiniPong 03#Weitere_Verbesserungsm.C3.B6glichkeiten|MiniPongCanvas03]]</code>
vor Allem hinsichtlich der Modularität leicht gebessert. Es besteht allerdings weiterer Verbesserungsbedarf.
 
{{Codequalität
| application    = MiniPongCanvas04
| readability    = 4
| writability    = 6
| continuity      = 4
| customizability = 4
| dry            = 3
| demeter        = 5
| verifiability  = 2
| interfaces      = 6
| contract        = 6
| liskov          = 6
| modularity      = 3
}}
 
==Das Prinzip der „[[Programmierprinzipien#Verst.C3.A4ndlichkeit.2C_Comprehesibility.2C_Lesbarkeit.2C_Readability|Verständlichkeit/Lesbarkeit]]“==
 
{{TBD}}
 
==Das Prinzip der „[[Programmierprinzipien#Schreibbarkeit.2C_Writability|Schreibbarkeit]]“==
 
{{TBD}}
 
==Das Prinzip der „[[Programmierprinzipien#Stetigkeit.2C_Continuity|Stetigkeit]]“==
 
{{TBD}}
 
==Das Prinzip der „[[Programmierprinzipien#Konfigurierbarkeit.2C_Customizability|Konfigurierbarkeit]]“==
 
{{TBD}}
 
==Das Prinzip „[[Don't repeat yourself]]“==
 
{{TBD}}
 
==Das „[[Programmierprinzipien#Gesetz_von_Demeter.5B5.5D.2C_Law_of_Demeter.2C_LoD|Gesetz von Demeter]]“==
 
{{TBD}}
 
==Das Prinzip der „[[Programmierprinzipien#.C3.9Cberpr.C3.BCfbarkeit.2C_Verifiability|Überprüfbarkeit]]“==
 
{{TBD}}
 
==Das Prinzip „[[Programmierprinzipien#Benutze_Integrit.C3.A4tsbedingungen.2C_Make_Use_of_Integrity_Constraints.2C_Design_by_Contract.5B2.5D|Design by Contract]]“==
 
{{TBD}}
 
==Das  „[[Programmierprinzipien#Liskovsches_Substitutionsprinzip.5B3.5D.2C_LSP.2C_Ersetzbarkeitsprinzip.2C_Liskov_substitution_principle.5B4.5D|Liskovsche Substitutionsprinzip]]“==
 
{{TBD}}
 
==Das Prinzip der „[[Programmierprinzipien#Modularit.C3.A4t.2C_Modularity.2C_Teile_und_herrsche.2C_Divide_et_impera|Modularität]]“==


{{TBD}}
{{TBD}}


=Quellen=
===Klassenmodell===
<references/>
[[Datei:MiniPong05 ClassModel Overview.png|gerahmt|rechts|Klassendiagramm]]
<ol start = "2">
<li>{{Quelle|Braun, H. (2011): Webanimationen mit Canvas}}</li>
<li>{{Quelle|Kowarschick, W.: Multimedia-Programmierung}}</li>
</ol>


=Fortsetzung des Tutoriums=


Sie sollten nun das [[HTML-Tutorium: SVG: Hello World|Hello-World-SVG-Tutorium]] bearbeiten, sofern Sie dies noch nicht gemacht haben.
==Quellen==
Anderenfalls können Sie gleich mit dem [[HTML-Tutorium: SVG: MiniPong|Minipong-SVG-Tutorium]] forfahren.
# {{Quelle|Kowarschick, W.: Multimedia-Programmierung}}
[[Kategorie: HTML5-Tutorium: Canvas: MiniPong]][[Kategorie: HTML5-Beispiel]][[Kategorie:Kapitel:Multimedia-Programmierung:Beispiele]]
[[Kategorie: HTML5-Tutorium: Canvas: MiniPong]][[Kategorie: HTML5-Beispiel]][[Kategorie:Kapitel:Multimedia-Programmierung:Beispiele]]

Aktuelle Version vom 9. Dezember 2016, 18:53 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_MiniPong05 (SVN))

Ziel: Das fertige Spiel „MiniPong“ verbessern

Im fünften Teil des Tutoriums wird beschrieben, wie die funktionsfähige Version von MiniPong verbessert und erweitert werden kann.

TO BE DONE

Klassenmodell

Klassendiagramm


Quellen

  1. Kowarschick (MMProg): Wolfgang Kowarschick; Vorlesung „Multimedia-Programmierung“; Hochschule: Hochschule Augsburg; Adresse: Augsburg; Web-Link; 2018; Quellengüte: 3 (Vorlesung)