HTML5-Tutorium: Canvas: MiniPong 05

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: Minipong 4 (SVN-Repository)

Ziel: Simulation von Klassen in JavaScript

Im vierten Teil des Tutoriums wird beschrieben, wie man Klassen in JavaScript nachbilden kann.

Vier Klassen werden implementiert: Ball, Paddle, Collision und Main. Es wird jeweils ein Objekt pro Klasse erstellt.

miniatur|ohne|709px|Das Datenmodell von MiniPong 04

Anwendung „MiniPongCanvas04

Neues Projekt anlegen

Legen Sie ein neues „Statisches Web-Projekt“ mit dem Namen MiniPongCanvas04 an.

Speichern Sie dieses Projekt wie üblich in Ihrem Repository.

Dateien erstellen

Erstellen Sie zunächst die Datei js/util.js und füegen Sie folgenden Code ein:

/**
 * @param {int} p_min
 * @param {int} p_max
 * @returns An integer number within the interval 
 *          [<code>p_min</code>, <code>p_max</code>].     
 */
Math.randomIntMinMax =
  function(p_min, p_max)
  { return (p_min == p_max)
           ? p_min
           : p_min+Math.floor(Math.random()*(p_max-p_min+1));    
  };

Damit haben Sie die Klasse Math um statische Funktion randomIntMinMax erweitert, die eine zufällige Integer-Zahl aus dem Intervall [p_min, p_max] berechnet.

Kopieren Sie die Dateien index.html, css/main.css und js/CONSTANT.js von Teil 3 des Tutoriums, passen Sie den Projekttitel in der Datei index.html 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 js/ball.js, js/paddle.js sowie js/collision.js. Sorgen Sie anschließend dafür, dass alle JavaScript-Dateien von der Datei index.html geladen werden, indem Sie folgende Zeilen in den Header dieser Datei einfügen:

<script type="text/javascript" src="js/util.js"     ></script>
<script type="text/javascript" src="js/CONSTANT.js" ></script>
<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>
<script type="text/javascript" src="js/main.js"     ></script>

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 (js/all.min.js) eingefügt werden. Natürlich darf in der Datei index.html anschließend auch nur noch diese eine JavaScript-Datei geladen werden.

Die Klasse Ball

Die Definition der Klasse Ball wird in JavaScript durch zwei Objekte realisiert oder – besser gesagt – simuliert: Eine Konstruktor-Funktion (genauer: ein Konstruktor-Funktion-Objekt) sowie das zugehörige prototype-Objekt.

Die Konstruktor-Funktion Ball erstellt neue Ball-Objekte und initialisiert diese, indem sie im jedem neu erstellten Objekt (this) Attribute erzeugt und initialisiert. (Ein Attribut wird automatisch erzeugt, sobald ihm ein Wert zugewiesen wird.)

Konstruktor

/** 
 *  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_START]
 *         The starting position of the ball in x-direction.
 *  @param {Number} [p_y_start = BALL_Y_START]     
 *         The starting position of the ball in y-direction.
 *  @param {Array[2]} [p_vx_start = BALL_VX_START] 
 *         The starting velocity of the ball in x-direction (min, max). 
 *  @param {Array[2]} [p_vy_start = BALL_VY_START]
 *         The starting velocity of the ball in y-direction (min, max).
 */
function Ball(p_context, p_r, p_x_start, p_y_start, p_vx_start, p_vy_start)
{ this.r = p_r || BALL_RADIUS;

  this.v_x_start  = p_x_start  || BALL_X_START;
  this.v_y_start  = p_y_start  || BALL_Y_START;
  this.v_vx_start = p_vx_start || BALL_VX_START; 
  this.v_vy_start = p_vy_start || BALL_VY_START; 

  this.v_context  = p_context;
  
  this.reset();
};

Die reset-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: prototype. Dieses Attribut enthält einen Verweis auf das prototype-Objekt des zugehörigen Konstruktors. JavaScript sucht bei einem Methodenaufruf obj.m() die Definition der Methode m zunächst im Objekt obj selbst. Sollte dort keine Definition vorhanden sein, so sucht JavaScript als nächstes in obj.prototype, dann in obj.prototype.prototype usw. Die Suche endet sobald entweder eine Definition gefunden oder kein weiteres prototype-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: Im Folgenden werden wieder JSDoc-konforme Kommentare benutzt.

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 velocity vector.
   */
  reset:
    function()
    { this.x  = this.v_x_start;
      this.y  = this.v_y_start;
      this.vx = (Math.random()<0.5?1:-1)
               *Math.randomIntMinMax(this.v_vx_start[0], this.v_vx_start[1]);
      this.vy = Math.randomIntMinMax(this.v_vy_start[0], this.v_vy_start[1]);
    },

  /** 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
  //////////////////////////////////////////////////////////////////////////////
};

Man beachte, dass die Getter- und Setter-Methoden für die Attribute r, x, y, vx, vy 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:

var my_ball = new Ball();

my_ball.r = 20;         // Aufruf der Setter-Methode
console.log(my_ball.r); // Aufruf der Getter-Methode

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:

/** The radius of the ball. */  
getR: function()    { return this.v_r; },
setR: function(p_r) { this.v_r = p_r;  },
...

In diesem Fall erfolgen die Attribut-Zugriffe mittels Methodenaufrufen:

var my_ball = new Ball();

my_ball.setR(20);            // Aufruf der Setter-Methode
console.log(my_ball.getR()); // Aufruf der Getter-Methode

Die Verwendung von echten Getter- und Setter-Methoden erhöht die 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.[1]

Beispielsweise könnten in der obigen Definition der Klasse Ball die Getter- und Setter-Methoden der Attribute x und y (ebenso wie alle anderen Getter- und Setter-Methoden) einfach aus dem Prototype-Objekt gelöscht werden. Am Verhalten der Klasse Ball würde das nichts ändern, da die Methoden reset und move nun direkt auf die Zustandsvariablen this.x und this.y 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 Logging-Befehl einfügen:

set x(p_x) { this.v_x = p_x; console.log("Setter 'x', param: " + p_x); },

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 müssen. Oder Sie hätten an jeder dieser Stellen ...ball.x = ... durch ...ball.setX(...) ersetzen müssen.

Die Klasse Paddle

Die Klasse Paddle wird analog zur Klasse Ball definiert. Die benötigten Attribute und Methoden findet man im obigen Klassendiagramm.

Die Methoden-Definitionen von reset, move und draw werden (wie bei der Klasse Ball aus) der Lösung des dritten Teils des Tutoriums übernommen. Bei der Definition der Methode reset sollte analog zur Klasse Ball der Zugriff auf globale Konstanten (PADDLE_X und PADDLE_Y) durch den Zugriff auf lokale Zustandsvariablen (this.v_x_start und this.v_y_start) ersetzt werden. Diese beiden Variablen sollten (wie in der Klasse Ball) von Konstruktor initialisiert werden.

Zusätzlich werden drei weitere Methoden benötigt:

/** 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; },

Die Attribute werden – bis auf vier Ausnahmen – genauso definiert wie in der Klasse Ball: Entweder gar nicht oder mittels echter Setter- und Getter-Methoden.

Vier Atribute wurden allerdings im Diagramm als nur lesbar (read only, frozen) deklariert: left, right, top und bottom. 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:

/** 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; },

Auf diese Attribute kann ebenfalls mit der üblichen Attribut-Syntax zugegriffen werden:

var my_paddle = new Paddle();

console.log(my_paddle.left); // Aufruf der Getter-Methode

Da keine Setter-Methoden existieren, können diese Attribute nicht geändert werden. Falls man seinen JavaScript-Code mittels "use strict"; als „strikt“ dekalriert hat, resultiert folgende Anweisung in einer Fehlermeldung:

my_paddle.left = 30; // Aufruf der nicht vorhandenen Setter-Methode

Die Klasse Collision

Ein Objekt der Klass Collision 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.

/** 
 *  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 {Object} p_walls  The walls of the game: An object with 
 *                           four integer attributes: <code>left</code>,
 *                           <code>right</code>, <code>top</code>, and
 *                           <code>bottom</code>.
 *  @param {Ball}   p_ball   The ball of the game. 
 *  @param {Paddle} p_paddle The paddle of the game.
 */
function Collision(p_walls, p_ball, p_paddle)
{ this.v_walls  = p_walls;
  this.v_ball   = p_ball;
  this.v_paddle = p_paddle;
};

In der Kollisionsklasse wird nur eine öffentliche Methode definiert: handleCollision. Diese Methode hat zwei Aufgabe: Kollisionen erkennen und erkannte Kollisionen zu behandeln. Dabei sind zwei Arten von Behandlung möglich:

  1. Die Kollisionspartner ändern ihr Verhalten (Richtungswechsel des Balls, Stopp des Schläger).
  2. Das Spiel reagiert auf die Kollisionen (Erhöhung der Punktezahl, Spielende)

Die erste Art der Behandlung nimmt handleCollision 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 (EVENT_SCORE: Erhöhung des Punktekontots, EVENT_EXIT: Spielende) über das eingetretene Ereignis. Es ist dann die Aufgabe des Aufrufers, entsprechende Maßnahmen vorzunehmen.

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_walls.bottom + this.v_ball.r);
    },

  // Tests whether the ball hits another wall.  
  m_collision_ball_wall:
    function()
    { if (   (this.v_ball.x <= this.v_walls.left  + this.v_ball.r)
          || (this.v_ball.x >= this.v_walls.right - this.v_ball.r)
         )
        this.v_ball.vx = -this.v_ball.vx;

      if (this.v_ball.y <= this.v_walls.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_walls.left  && this.v_paddle.vx < 0)
          || (this.v_paddle.right >= this.v_walls.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;
    },

};

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 main.js

Hier könnte zunächst die Datei main.js von Teil 3 des Tutoriums übernommen und leicht angepasst werden:

Die Variablendefinition von g_ball und g_paddle werden durch Variablendeklarationen ersetzt:

// ball, paddle, collision
var g_ball;
var g_paddle;
var g_collision;

Die eigentlich Definition der drei Objekte erfolgt nun in der Funktion f_init nachdem das Objekt g_context ermittelt wurde:

// Initialize all game objects.
g_ball      = new Ball(g_context);
g_paddle    = new Paddle(g_context);
g_collision = new Collision
                  ({left:   0, 
                    right:  CANVAS_WIDTH, 
                    top:    0,
                    bottom: CANVAS_HEIGHT
                   }, 
                   g_ball, 
                   g_paddle
                  );

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

g_ball.draw(g_context);
g_paddle.draw(g_context);

werden durch

g_ball.draw();
g_paddle.draw();

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:

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();
}

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:

// 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(); }

Eine echte Main-Klasse (main.js reloaded)

Die JavaScript-Datei main.js enthält noch viele globale Werte: globale Variablen (g_...), globale Funktionen (f_...) und globale Observer-Methoden (o_...).

Aus Sicht des „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 main.js sollten in einem Main-Objekt gekapselt werden.

Konstruktor

Der Konstruktor übernimmt im Wesentlichen die Aufgaben der ehemaligen Init-Funktion f_init. Anmerkung: Hier wurde bewusst auf JSDoc-Kommentare verzichtet, das Main eine „private“ Klasse ist, die nicht in anderen Projekten wiederverwendet wird. Daher ist es nicht sonderlich sinnvoll, die API dieser Klasse automatisch zu generieren.

// Create 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
                         ({left:   0, 
                           right:  CANVAS_WIDTH, 
                           top:    0,
                           bottom: CANVAS_HEIGHT
                          }, 
                          this.v_ball, 
                          this.v_paddle
                         );
  
  var self = this; // Needed to overcome dynamic 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(); 
}

Im obigen Datenmodell wurde die Klasse Main als Singleton deklariert. Das heißt, von dieser Klasse darf es zu jedem Zeitpunkt maximal ein Objekt geben. Die erste if-Anweisung im Konstruktor sorgt dafür, dass damit tatsächlich nur ein Main-Objekt erzeugt werden kann. Das erste Main-Objekt wird im Konstruktor-Objekt gespeichert. Sofern tatsächlich versucht werden würde, ein zweites Main-Objekt mit diesem Konstruktor zu erzeugen, wird einfach das schon existente Main-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 this nicht vom Definitionsort ab, sondern vom Aufrufer der Methode. Die Variable this ist also – im Gegensatz zu allen anderne JavaScript-Variablen – nicht lexikalisch sondern dynamisch gebunden.

Das Problem ist nun, dass die Methode o_start_stop_game nicht jetzt, sondern erst später aufgerufen wird und zwar vom im HTML-Dokument enthaltenen Button-Objekt (document.getElementById("d_start_stop")) sobald der Benutzer auf diesen Button klickt. Damit zeigt die Variable this auf dieses Button-Objekt und nicht auf das Main-Objekt. Da aber die Methode o_start_stop_game mittels this auf Attribute des Main-Objekts zugreifen will, muss man einen schmutzigen, aber in JavaScript-Code häufig anzutreffenden Trick anwenden. Man muss this 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 self.

var self = this; // Needed to overcome dynamic binding problems.

Innerhalb der Funktion, die das Button-Objekt später aufruft, wird nun self anstelle von this verwendet.

document.getElementById("d_start_stop").onmousedown =
  function() { self.o_start_stop_game(); };

Da self lexkalisch gebunden ist, tritt das zuvor beschriebene Problem nicht mehr auf. Wenn nun innerhalb der Methode o_start_stop_game auf die Variable this zugegrifen wird, so ist darin wie gewünscht das Main-Objekt enthalten.

Warnung: Dieses JavaScript-Verhalten kann gerade einem Programmieranfäger stundenlange Fehlersuche bescheren.

Methoden

Die ehemals öffentlichen Funktionen der JavaScript-Datei main.js werden als Methoden des Main-Objekts realisiert.

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 dynamic binding problems.

      // React to key down and key up events.
// The following code does not work due to dynamic 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
  //////////////////////////////////////////////////////////////////////////////
};

Beachte: Auch in den Methodendefinitionen musste der „this/self-Hack“ mehrfach angewandt werden. Man überlege sich in allen Fällen, warum dies notwendig ist.

Um das Problem nachzuvollziehen, sollten Sie die Trace-Anweisung console.log(this); in der Methode o_start_paddle_moving (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 m_init_started die ersten beiden der folgenden Zeilen und kommentieren Sie die nachfolgenden vier Zeilen aus:

//    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); };

Aus obigem Code wird also:

      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); };

Starten Sie nun Ihre Anwendung erneut und versuchen Sie den Schläger mittels Tastensteuerung zu bewegen.

Sie werden feststellen, dass die Methode o_start_paddle_moving nun nicht mehr auf das Main-Objekt zugreift, sondern auf das index.html-Objekt (welches JavaScript in der globalen Variablen document zur Verfügung stellt). Die in diesem Objekt enthaltenen Methoden onkeydown und onkeyup werden von JavaScript aufgerufen, sobald der Benutzer irgendeine Taste auf dem Keyboard drückt bzw. loslässt. Da im index.html-Objekt das Objekt v_paddle nicht existiert, reagiert MiniPong nun bei jedem Tastendruck mit einer Fehlermeldung.

In der ursprünglichen Version von main.js bestand dieses Problem noch nicht, da alle Funktionen (wie z.B. o_start_paddle_moving) und Variablen global definiert wurde. Im globalen Kontext bezieht sich this 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)

TO BE DONE

miniatur|ohne|945px|Das Objektmodell von MiniPong 04a

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

Verbesserungsmöglichkeiten

Die Code-Qualität von MiniPongCanvas04 hat sich gegenüber MiniPongCanvas03 vor allem hinsichtlich der Modularität leicht gebessert. Es besteht allerdings weiterer Verbesserungsbedarf.

Bei der Realisierung der Anwendung MiniPongCanvas04 wurden wichtige Programmierprinzipien nur eingeschränkt beachtet:
  ★★★★☆ Verständlichkeit/Lesbarkeit: sehr gut
  ★★★★☆ Stetigkeit: sehr gut
  ★★★★☆ Konfigurierbarkeit: sehr gut
  ★★★☆☆ DRY: wenige Wiederholungen
  ★★★★★ Gesetz von Demeter: wurde beachtet
  ★★☆☆☆ Überprüfbarkeit: eine simple formale Spezifikation ist vorhanden
  ★★★☆☆ Modularität: modular (die wesentlichsten Regeln wurden beachet)

<ul><li>„6“ befindet sich nicht in der Liste (0, 1, 2, 3, 4, 5) zulässiger Werte für das Attribut „Codequalität:Schreibbarkeit“.</li> <!--br--><li>„6“ befindet sich nicht in der Liste (0, 1, 2, 3, 4, 5) zulässiger Werte für das Attribut „Codequalität:Interfaces“.</li> <!--br--><li>„6“ befindet sich nicht in der Liste (0, 1, 2, 3, 4, 5) zulässiger Werte für das Attribut „Codequalität:Integritätsbedingungen“.</li> <!--br--><li>„6“ befindet sich nicht in der Liste (0, 1, 2, 3, 4, 5) zulässiger Werte für das Attribut „Codequalität:Ersetzbarkeitsprinzip“.</li></ul>

Das Prinzip der „Verständlichkeit/Lesbarkeit

TO BE DONE

Das Prinzip der „Schreibbarkeit

TO BE DONE

Das Prinzip der „Stetigkeit

TO BE DONE

Das Prinzip der „Konfigurierbarkeit

TO BE DONE

Das Prinzip „Don't repeat yourself

TO BE DONE

Das „Gesetz von Demeter

TO BE DONE

Das Prinzip der „Überprüfbarkeit

TO BE DONE

Das Prinzip „Design by Contract

TO BE DONE

Das „Liskovsche Substitutionsprinzip

TO BE DONE

Das Prinzip der „Modularität

TO BE DONE

Quellen

  1. vgl. Meyer (1997): Bertrand Meyer; Object-oriented Software Construction; Auflage: 2; Verlag: Prentice Hall International; ISBN: 0136291554; 1997; Quellengüte: 5 (Buch)
  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 das Hello-World-SVG-Tutorium bearbeiten, sofern Sie dies noch nicht gemacht haben. Anderenfalls können Sie gleich mit dem Minipong-SVG-Tutorium forfahren.