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

aus GlossarWiki, der Glossar-Datenbank der Fachhochschule Augsburg
Kowa (Diskussion | Beiträge)
Keine Bearbeitungszusammenfassung
Kowa (Diskussion | Beiträge)
Keine Bearbeitungszusammenfassung
Zeile 449: Zeile 449:
* Es gibt keine überflüssigen Leerzeilen.
* Es gibt keine überflüssigen Leerzeilen.
* Es gibt keine Tab-Symbole, damit der Code auch mit anderen Text-Programmen (wie z.B. in einem Browser) gut gelesen werden kann.
* Es gibt keine Tab-Symbole, damit der Code auch mit anderen Text-Programmen (wie z.B. in einem Browser) gut gelesen werden kann.
* Die Bezeichner von Variablen, Funktionen etc. unterscheiden sich syntaktisch so, dass man ihre Aufgabe sofort erkennt. Beispielsweise handelt es sich bei <code>g_ball</code> um eine globale Variable, bei <code>p_event</code> um einen [[Parameter]] und bei <code>CANVAS_WIDTH</code> um eine Konstante (vgl. [[Multimedia-Programmierung: Konventionen|Programmierkonventionen]]).
* Die Bezeichner von Variablen, Funktionen etc. unterscheiden sich syntaktisch so, dass man ihre Aufgabe sofort erkennt. Beispielsweise handelt es sich bei <code>g_ball</code> um eine globale Variable, bei <code>p_event</code> um einen [[Parameter]] und bei <code>CANVAS_WIDTH</code> um eine Konstante (vgl. [[Multimedia-Programmierung: Style Guide|Style Guide]]).
* Die Namen der Variablen, Parameter, Funktionen, Attribute, Methoden und Konstanten sind selbsterklärend. Beispielsweise wird in der Variablen <code>g_ball</code> ein Ballobjekt gespeichert.
* Die Namen der Variablen, Parameter, Funktionen, Attribute, Methoden und Konstanten sind selbsterklärend. Beispielsweise wird in der Variablen <code>g_ball</code> ein Ballobjekt gespeichert.
* Der Code wurde mit sinnvollen Kommentaren versehen (für fortgeschrittene Programmierer sollte die Zahl allerdings etwas reduziert werden).
* Der Code wurde mit sinnvollen Kommentaren versehen (für fortgeschrittene Programmierer sollte die Zahl allerdings etwas reduziert werden).

Version vom 25. Oktober 2014, 12:46 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: Minipong 3 (SVN-Repository)

Ziel: Implementierung von MiniPong mit Schläger, Ball und Punktanzeige

Im dritten Teil des Tutoriums wird beschrieben, wie man MiniPong mit Hilfe einer einzigen JavaScript-Datei main.js erstellt (wenn man von der Auslagerung der Konstanten in eine eigene „Initialisierungsdatei“ einmal absieht). Wolfgang Kowarschick bezeichnet so eine Datei als „Moloch-Datei“ (Moloch = Monstrum, Ungeheuer, alles verschlingende Bestie).

Drei Objekte werden implementiert: Das ROOT-Objekt der Anwendung, ein Ball-Objekt und ein Schläger-Objekt.

Das Objektmodell von MiniPong 03

Anmerkung: Darüber hinaus gibt es noch ein Info-Objekt, zur Ausgabe von Informationen über den Spielverlauf. Das sich dieses Text-Objekt jedoch nicht auf der Bühne befindet, wurde es im obigen Modell nicht aufgeführt. Das heißt, das obige Modell ist genaugenommen unvollständig.

Anwendung „MiniPongCanvas03

Neues Projekt anlegen

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

Speichern Sie dieses Projekt wie üblich in Ihrem Repository.

Dateien erstellen

Kopieren Sie die Dateien index.html und css/Main.css von Teil 2 des Tutoriums, passen Sie den Projekttitel in der Datei index.html an und erstellen Sie die Dateien js/CONSTANT.js js/Main.js neu.

index.html

Fügen Sie in den HTML-Rumpf (body) folgenden Code vor das canvas-Element ein:

<p id="d_info">&nbsp;</p>

Der Text dieses Elements wird von JavaScript aus geändert, um den jeweiligen Punktestand anzuzeigen. &nbsp; ist ein nicht-trennbares Leerzeichen (Non-Breaking SPace) und sorgt dafür, dass sich Eclipse nicht über ein leeres p-Element beschwert.

Fügen Sie in den HTML-Rumpf (body) folgendes Formular hinter das canvas-Element ein:

<form>
  <input id="d_start_stop" type="button" value=" "/>
</form>

Dieser Button fungiert – je nach aktuellen Status des Spiels – als Start- oder Stopp-Button. Die Beschriftung sowie die Zuordnung der zugehörigen Aktion wird jeweils mittels JavaScript an den aktuellen Zustand der Anwendung angepasst.

Das Attribut value muss laut HTML5-Standard vorhanden sein. Dessen Wert dar nicht „leer“ sein. Daher wird es hier angegeben, obwohl es eigentlich unnötig ist, da der Wert sofort von JavaScript überschrieben wird.

CONSTANT.js

Fügen Sie alle Variablen der beiden Dateien CONSTANT.js von Teil 1 und Teil 2 des Tutoriums in die Datei CONSTANT.js ds dritten Teils ein.

Achtung: Definieren Sie keine Variable doppelt.

Sie werde später noch weitere Konstanten in diese Datei einfügen, wie z.B. Konstanten, die die Startposition von Ball und Schläger definieren, oder eine Konstate PADDLE_FRICTION, die die Reibung des Schlägers festlegt. (Beachten Sie, dass in den Use Cases Folgendes gefordert wurde: Wird der Schläger im Moment der Kollision bewegt, so wird der Ball abhängig von der Bewegungsrichtung und Geschwindigkeit des Schlägers abgelenkt.)

main.js

Fügen Sie die Variablen g_ball, g_paddle, g_context, g_imterval der beiden Dateien main.js von Teil 1 und Teil 2 des Tutoriums in die Datei main.js des dritten Teils ein. Definieren Sie zusätzlich eine globale Variable g_score, in der der aktuelle Punktestand des Spiels abgelegt wird.

Das Ball-Objekt g_ball

Erweitern Sie das Ball-Objekt g_ball um eine Resetmethode, deren Aufgabe es ist, den Ball vor Spielbeginn zur Startposition zu verschieben. Außerdem muss diese Funktion eine Startgeschwindikeit des Balls festlegen.

// Moves the ball to its starting position 
// and computes randomly a starting velocity vector.
reset: 
  function()
  { this.x  = BALL_X;
    this.y  = BALL_Y;
    this.vx =   (Math.random()<0.5?1:-1)
              * (BALL_VX_MIN+Math.random()*(BALL_VX_MAX-BALL_VX_MIN));
    this.vy =   (BALL_VY_MIN+Math.random()*(BALL_VY_MAX-BALL_VY_MIN));
  },

Definieren Sie die Konstanten BALL_X und BALL_Y in der Datei CONSTANT.js so, dass der Ball in der Mitte des oberen Spielfeldrands zum liegen kommt. Legen Sie für die übrigen Konstanten sinnvolle Werte fest.

Legen Sie im Ball-Objekt für alle Attribute sinnvolle Initialwerte fest. Diejenigen Werte, die später von der reset-Methode gesetzt werden, können einfach mit 0 oder null vorbelegt werden.

Das Schläger-Objekt g_paddle

Erweitern Sie das Schläger-Objekt g_paddle um eine Resetmethode, deren Aufgabe es ist, den Schläger auf die Startposition (die ebenfalls in der Datei CONSTANT.js definiert wird) und die Startgeschwindigkeit des Schlägers (in x-Richtung) auf 0 zu setzen:

// Moves the paddle to its starting position 
// and sets its velocity to zero.
reset: 
  function()
  { this.x  = PADDLE_X;
    this.y  = PADDLE_Y;
    this.vx = 0;
  },

Legen Sie im Schläger-Objekt für alle Attribute sinnvolle Initialwerte fest. Diejenigen Werte, die später von der reset-Methode festgelegt werden, können einfach mit 0 oder null vorbelegt werden.

Definieren Sie für das Schläger-Objekt ein weiteres Attribut friction und initialisieren Sie dieses mit der Konstanten PADDLE_FRICTION. Der Wert dieser Konstanten sollte ca. 0.3 betragen.

Die Draw-Funktion

Die Draw-Funktion f_draw hat nur eine Aufgabe: Das Neuzeichnen der Bühne, d.h., das Löschen des Bühneninhalts und das Neuzeichnen aller Sprites auf der Bühne. Dazu ruft sie der Reihe nach die draw-Methoden der einezelnen Sprites auf und übergibt diesen die Bühne auf der der jeweilige Sprite gezeichnet werden soll. (In unseren Fall gibt es nur zwei Sprites: Ball und Schläger.)

// Clears the canvas and redraws all sprites (ball and paddle).
function f_draw()
{ g_context.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);

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

Zusätzlich sollte man noch eine Funktion definieren, mit der man Informationen in das Textfeld der HTML-Seite schreiben kann:

// Displays information on the HTML page.
function f_info(p_info)
{ document.getElementById("d_info").firstChild.nodeValue = p_info; }

Die Init-Funktion

Die Funktion f_init unterscheidet sich nicht wesentlich von der Init-Funktion der beiden ersten Teile des Tutoriums.

Neu ist lediglich der Aufruf der Observer-Funktion o_stop_game und das Fehlen der Timerdefinition. Der Animations-Timer wird im Spiel erst gestartet, wenn der Spieler den Start-Knopf gedrückt hat. Die Stopp-Funktion hat die Aufgabe, das Spiel in die Ausgangssituation zu bringen (siehe Definition dieser Funktion am Ende des Artikels). Sie wird im weiteren Spielverlauf jedesmal dann aufgerufen, wenn ein Spiel durch einen Spielfehler oder durch den Spieler beendet wird.

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

  // Reset the game (i.e. change its status to "waiting for game to be started").
  o_stop_game();
}

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

Observer

Observer sind speziele Funktionen, die in Falle von betimmten Ereignissen (events), wie Timer-Event, Tasten-Event, Maus-Event etc. ausgeführt werden.

Die beiden Observer o_start_paddle_moving und o_stop_paddle_moving können direkt aus der Datei main.js von Teil zwei des Tutoriums übernommen werden. Sie haben die Aufgabe, den Schläger in eine bestimmte Richtung zu bewegen, solange der Spieler eine bestimmte Taste drückt.

Der Observer o_redraw wächst im Teil 3 des Tutoriums zum „Moloch“ an. Er hat die Aufgabe, eine Kollision zwischen Ball und Wand oder Ball und Schläger zu erkennen und entsprechend zu reagieren:

  • Ball und Wand (links, rechts, oben): Ball prallt ab
  • Ball und Wand (unten außerhalb des sichbaren Bereichs): Spielende
  • Ball und Schläger: Ball prallt ab und wird evtl. abgelenkt; Erhöhung des Scores um einen Punkt

Außerdem muss diese Funktion auch noch den Canvas löschen und die neu positionierten Sprites zeichnen, sofern die Funktion nicht aufgrund des Spielendes vorzeitig verlassen wurde.

//  An event observer: 
//  It is called every 1000/FPS milliseconds, but only when the game has been started.
function o_redraw() 
{ // Collision detection: ball <-> wall (left, right, top) 
  // Collision response:  change the moving direction of the ball
  if (g_ball.x <= g_ball.r || g_ball.x >= CANVAS_WIDTH - g_ball.r)
  { g_ball.vx = -g_ball.vx; }
  if (g_ball.y <= g_ball.r)
  { g_ball.vy = -g_ball.vy; }

  // Collision detection: ball <-> wall (bottom outside of the canvas) 
  // Collision response:  stop the game (as the ball has vanished)
  if (g_ball.y - 2*g_ball.r > CANVAS_HEIGHT)
  { o_stop_game();
    return; // leave o_redraw early
  }
 
  // Collision detection: ball <-> paddle
  // Collision response:  change the moving direction of the ball 
  //                    + change the velocity of the ball 
  //                      (due to the friction between the ball and the paddle)
  //                    + display the new score
  // Collision detection and handling has to be improved:
  // A collision with the small side of the paddle should be handled.
  if (   g_ball.y + g_ball.r     >= g_paddle.y 
      && g_ball.y + g_ball.r     <= g_paddle.y + g_paddle.height
      && g_ball.x + 0.5*g_ball.r >= g_paddle.x
      && g_ball.x - 0.5*g_ball.r <= g_paddle.x + g_paddle.width
     )
  { // Resolve penetration.
    if (g_ball.vy > 0)                 // The ball is moving from top to bottom.
    { g_ball.y = g_paddle.y - g_ball.r; }
    else                               // The ball is moving from bottom to top.
    { g_ball.y = g_paddle.y + g_paddle.height + g_ball.r; }
  
    g_ball.vy = -g_ball.vy;
    g_ball.vx += g_paddle.friction*g_paddle.vx;
    
    // Raise the score and display it.
    g_score++;
    f_info("Punkte: " + g_score);
  }
  
  // Move the ball.
  g_ball.move();
  
  // Move the paddle if possible.
  if (   (g_paddle.vx < 0 && g_paddle.x > 0)
      || (g_paddle.vx > 0 && g_paddle.x + g_paddle.width < CANVAS_WIDTH)
     )
  { g_paddle.move(); }

  // Redraw the canvas.
  f_draw();
}

Im Spiel gibt es noch zwei weitere Observer. Der Observer o_start_game wird bei einem Klick auf den Start/Stopp-Button aufgerufen, falls diese als Start-Button fungiert. Seine Aufgabe ist es, den Score auf Null zurückzusetzen, den Start/Stopp-Button als Stopp-Button zu aktivieren, die Steuerung des Schlägers zu aktivieren, den Animations-Timer der „physics engine“ zu starten und zu guter Letzt den eine Information über den Spielstart in das Textfeld auf der Bühne zu schreiben. Aktuell könnte im letzten Schritt auch gleich der aktuelle Punktestand (nnull Punkte) angezeigt werden.

// An event observer: 
// It is called whenever a "start game event" is signaled.
function o_start_game() 
{ // Reset the score.
  g_score = 0;

  // Set the button "d_start_stop" to be a stop button (view and behaviour).
  document.getElementById("d_start_stop").value       = "Stopp";
  document.getElementById("d_start_stop").onmousedown = o_stop_game;

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

  // Show the current score (zero points)
  f_info("Punkte: " + g_score);
}

Beachten Sie, dass der Code f_info("Punkte: " + g_score); an zwei Stellen im Programm aufgerufen wird. Damit ist der Code nicht mehr DRY. Wenn die Anzeige der Punkte veränder werden soll, muss dies an zwei Stellen erfolgen. Wie kann man den Code verbessern?

Die Aufgabe des Stopp-Spiel-Observers ist im Wesentlichen, die Aktionen des Start-Spiel-Servers rückgängig zu machen:

  • Der Animations-Timer wird gestoppt.
  • Die Schläger-Steuerungstasten werden deaktiviert.
  • Der Start-Stop-Button wird als Start-Button aktiviert.

Außerdem werden der Ball un der Schläger an ihre jeweiligen Startpositionen gesetzt. Anschließend muss die Bühne neu gezeichnet werden, damit die neuen Positionen dieser biden Sprites auch sichtbar wird.

// An event observer: 
// It is called whenever a "stop game event" is signaled.
function o_stop_game()
{ // Stop the canvas redrawing timer.
  window.clearInterval(g_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 and behaviour).
  document.getElementById("d_start_stop").value       = "Start";
  document.getElementById("d_start_stop").onmousedown = o_start_game;

  // Put the ball and the paddle on their starting positions
  // and then redraw the canvas.
  g_ball.reset();
  g_paddle.reset();
  f_draw();
}

Man beachte, dass die Befehle in umgekehrter Reihenfolge wie in der Start-Funktion ausgeführt werden. Dies wird bei komplementären Start-/Stop-Anweisungen üblicherweise so gemacht, da es bei dieser Reihenfolge der Stop-Anweisungen i. Allg. nicht zu Abhängigkeitsfehlern kommt. Wenn beispielsweise die Anweisung B nur dann funktioniert, wenn das Ergebnis der Anweisung A vorliegt, dann muss A vor B gestartet aber nach B beendet werden.

Man beachte außerdem, dass die Stop-Funktion den Score nicht verändert, damit der Spieler die erzielten Punkte auch nach Spielende noch lesen kann. Hier ist allerdings noch ein kleiner Fehler enthalten.

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, d.h., dass zwei Spieler gegeneinander spielen können. Jeder Spieler hat seinen eigenen Score. Der erste Spieler, der 7 Punkte erreicht, gewinnt.

Am besten zeigen Sie die erzielten Punkte auch auf der Bühne selbst an (vgl. Bilder im Wikipedia-Artikel zu Pong). Wie man Text auf der Bühne darstellen kann, wurde im Hello-World-Tutorium gezeigt.

Das Objektmodell von MiniPong 03a

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

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

Siehe auch: http://www.guimp.com/pong_flash.html; Hier ist der Name MiniPong wirklich gerechtfertigt.

Probleme der Kollisionserkennung

Die in Teil 1 des Tutoriums angesprochenen Probleme de „Tunnelns“ und der „Überlappung“ bei einer A-posteriori-Kollissionserkennung kann man in dieser Minipong-Version besonders schön veranschaulichen.

Tunneln

Beispiel: modifizierte Minipong-Anwendung

In diesem Beispiel wurden einfach die Framrate deutlich reduziert (auf 10 Frames pro Sekunde) sowie eine hohe Ballgeschwindigkeit (200 Pixel pro Sekunde in y-Richtung) eingestellt.

(Damit Sie den Effekt auf jeden Fall beobachten können, wurde außerdem die Steuerung des Schlägers deaktiviert.)

Überlappung

Beispiele: modifizierte Minipong-Anwendung, modifizierte Pong-Anwendung,

Um diesen Effekt zu erreichen wurde nur eine wesentliche Änderung am Source-Code vorgenommen. In der Funktion o_redraw wurden folgende fünf Zeilen auskommentiert:

// Resolve penetration.
if (g_ball.vy > 0)                 // The ball is moving from top to bottom.
  g_ball.y = g_paddle.y - g_ball.r;
else                               // The ball is moving from bottom to top.
  g_ball.y = g_paddle.y + g_paddle.height + g_ball.r;

Diese Anweisungen verschieben den Ball im Falle einer Kollision soweit, dass er den Schläger nicht überlappt, sondern nur berührt. Wenn diese Korrekturmaßnahme unterbleibt, kann der Ball in gewissen Situationen im Schläger hängen beleiben. Eine derartige Situation kann man beispielsweise erzwingen, wenn man in der Datei CONSTANT.js der Minipong-Anwendung folgende Werte wählt:

BALL_VX_MIN  = 116, // pixels per second
BALL_VX_MAX  = 116, // pixels per second
BALL_VY_MIN  =  21, // pixels per second
BALL_VY_MAX  =  21, // pixels per second

Aber auch ohne Modifikation der Startwerte des Balls kann dieser Effekt auftreten, wenn man den Ball während des Spiels geeignet mit dem Schläger ablenkt.

Weitere Verbesserungsmöglichkeiten

Der hier vorgestellte Minipong-Code realisiert alle in der Use-Case-Beschreibung geforderten Anwendungsfälle:

Datei:MiniPongUseCases.png
Use Cases der Tutoriums-Anwendung Minipong

Der Code ist dennoch nicht zufriedenstellend, da viele Programmierprinzipien verletzt werden. Auf die Beachtung dieser Prinzipien könnte bei Mini-Anwendungen wie MiniPong sicherlich verzichtet werden, da der Aufwand größer ist als der Ertrag. Bei größeren Anwendungen sollte man dies Prinzipien allerdings tunlichst beachten, da man anderenfalls mit großer Wahrscheinlichkeit keine robust laufende Anwendung und keinen wiederverwendbaren Code erhält.

Bei der Realisierung der Anwendung MiniPongCanvas03 wurden wichtige Programmierprinzipien nicht beachtet:
  ★★★★☆ Verständlichkeit/Lesbarkeit: sehr gut
  ★★★☆☆ Schreibbarkeit: gute Entwicklungsumgebung
  ★★★★☆ Stetigkeit: sehr gut
  ★★★★☆ Konfigurierbarkeit: sehr gut
  ★★★☆☆ DRY: wenige Wiederholungen
  ★★★★★ Gesetz von Demeter: wurde beachtet
  ★★☆☆☆ Überprüfbarkeit: eine simple formale Spezifikation ist vorhanden
  ★★★★★ Interfaces: vollständig spezifiziert
  ☆☆☆☆☆ Integritätsbedingungen: nicht vorhanden
  ★★★★★ Ersetzbarkeitsprinzip: wurde beachtet
  ★☆☆☆☆ Modularität: kaum modular

Verstöße gegen das „Prinzip der Verständlichkeit/Lesbarkeit

Es gibt nur einen wesentlichen Verstoß gegen das Prinzip „Verständlichkeit/Lesbarkeit“:

  • Die Kommentare wurden nicht im JSDoc-Format geschrieben.

Dieses Format kann verwendet werden, um eine API-Dokumentation automatisch zu generieren.

Ansonsten wurde versucht, den Code so lesbar und verständlich wie möglich zu gestalten:

  • Die Breite des Codes wurde fast überall auch 80 Zeichen beschränkt, damit er auf normalem Dia-A4-Papier in normaler Schriftgröße ausgedruckt werden kann.
  • Der Code wurde sauber und platzsparend eingerückt.
  • Klammerpaare werden leicht als solche erkannt.
  • Es gibt keine überflüssigen Leerzeilen.
  • Es gibt keine Tab-Symbole, damit der Code auch mit anderen Text-Programmen (wie z.B. in einem Browser) gut gelesen werden kann.
  • Die Bezeichner von Variablen, Funktionen etc. unterscheiden sich syntaktisch so, dass man ihre Aufgabe sofort erkennt. Beispielsweise handelt es sich bei g_ball um eine globale Variable, bei p_event um einen Parameter und bei CANVAS_WIDTH um eine Konstante (vgl. Style Guide).
  • Die Namen der Variablen, Parameter, Funktionen, Attribute, Methoden und Konstanten sind selbsterklärend. Beispielsweise wird in der Variablen g_ball ein Ballobjekt gespeichert.
  • Der Code wurde mit sinnvollen Kommentaren versehen (für fortgeschrittene Programmierer sollte die Zahl allerdings etwas reduziert werden).

Verstöße gegen das „Prinzip der Schreibbarkeit

Die Entwicklungsumgebung ist in mancherlei Hinsicht nicht zufriedenstellend:

  • Juno ist deutlich träger als Indigo. Diese Erfahrung haben neben Wolfgang Kowarschick anscheinend mehrere Benutzer gemacht (siehe z.B. [1], [2], [3]).
  • Juno ist anscheinend auch hinsichtlich der Fehlerkorrektur und der Autovervollständigung ein Rückschritt gegenüber Indigo (eigene Erfahrung von Wolfgang Kowarschick).

Zur Lösung dieser Probleme sollte man versuchsweise auf Indigo umsteigen (auch wenn auf http://www.eclipse.org/ Juno defaultmäßig zum Download angeboten wird). Eine Alternative wäre evtl. auch die Entwicklungsumgebung Netbeans mit der ich (Wolfgang Kowarschick) sehr gute Erfahrungen hinsichtlich der Entwicklung Web-Anwendungen gemacht habe.

Aber selbst wenn man diejenige Entwicklungsumgebung gefunden hat, mit der man persönlich am Besten zurechtkommt, gibt es weitere Verbesserungsmöglichkeiten:

  • Für Code-Schnipsel, die man häufig verwendet, sollte man Tastaturkürzel definieren. Ein sehr interessantes Werkzeug für diesen Zweck ist Zen Coding (siehe auch Aptana on Steroids using Zen Coding und eclipse-zencoding).
  • Für Dateien (html, js, css ...) sollte man sich Templates anlegen bzw. die Templates der verwendeten Entwicklungsumgebung an seine Vorlieben anpassen.
  • Das Grundgerüst des zu implementierenden Codes (Objekt-, Klassen-, Attribut-, Methoden-Definitionen etc.) sollte automatisch generiert werden (z.B. mit Hilfe eines UML-Werkzeugs).
  • Für Code und Modell gemeinsam sollte Refactoring verfügbar sein. Das heißt, es sollte jederzeit problemlos möglich sein, die Namen von Klassen, Objekten, Methoden, Attributen etc. konsistent in allen Dateien zu ändern.

Verstöße gegen das „Prinzip der Stetigkeit

Ein Programm-Code heißt stetig, wenn kleine Änderungswünsche auch nur kleine Änderungen am Code zur Folge haben.

Dieses Prinzip wird relativ gut beachtet. Man vergleiche beispielsweise den Code der MiniPong-Datei main.js der Canvas-Version mit dem Code von main.js der SVG-Version:

  1. Beide Musterlösungen (Canvas-Version, SVG-Version) in Eclipse laden
  2. Im Projektexplorer: beide main.js-Dateien selektieren
  3. Rechtsklick auf eine der beiden Dateien → Compare withEinander

Der Änderungswunsch lautete „Ersetze die Canvas-Bühne durch eine SVG-Bühne“. Der in zwei Tutorien (HTML5-Tutorium: Canvas: MiniPong 03 und HTML-Tutorium: SVG: MiniPong 03) erstellte Code unterscheidet sich nicht sonderlich stark.

Hinsichtlich des Prinzips „Don't repeat yourself“ sind allerdings noch einige Verbesserungen möglich (siehe unten).

Verstöße gegen das „Prinzip der Konfigurierbarkeit

Ein wichtiges Prinzip, um Stetigkeit zu erzielen, ist, die einfache Konfigurirbarkeit der Anwendung sicherzustellen. Konstanten sollten nie im Code stehen, sondern immer in einer eigenen Konfigurationsdatei.

Gegen dieses Prinzip wird nur in einer Hinsicht verstoßen:

Texte, mit denen der Benutzer informiert wird (Start, Stopp, Punkte) stehen direkt im Code.

(Anmerkung: In der Datei main.js der zu Pong erweiterten Version von MiniPong sind noch deutlich mehr Informationstexte direkt im Code enthalten.)

Alle anderen Konstanten wurden in die Datei CONSTANT.js ausgelagert, die somit für MiniPong als Konfigurationsdatei fungiert.

In eine künftigen MiniPong-Version sollten die Informationstexte auch in eine Informationsdatei ausgelagert werden. Im Rahmen dieser Auslagerung sollte MiniPong gleich „internationalisiert“ werden. Das heißt, die Informationstexte sollten jeweils in der Sprache von Benutzer bevorzugten Sprache ausgeliefert werden.

Verstöße gegen das Prinzip „Don't repeat yourself

Die Beactung des Prinzips „Don't repeat yourself“ (DRY) ist ein weiteres wichtiges Instrument, um die Stetigkeit von Programmcode zu erhöhen.

Der Minipong-Code ist nicht DRY.

Beispiel: Der Code f_info("Punkte: " + g_score); wird an zwei Stellen im Programm aufgerufen. Wenn irgendwann einmal der Text der Punktausgabe verändert werden soll, muss dies an zwei Stellen erfolgen. Ändert man den Text nur an einer Stelle, so verhält sich das Programm danach inkonsitstent.

Es gibt aber noch eine subtilere Form der Wiederholung von Code: Die grafischen Objekte (Ball, Schläger) enthalten gleiche Attribute (z.B. Position x, y) und gleiche Methoden (move, draw) mit vergleichbaren Aufgaben. Die Methode move könnte beispielsweise für alle grafischen Objekte identisch definiert werden:

move: 
  function()
  { this.x  += this.vx/FPS;
    this.y  += this.vy/FPS;
    this.vx += this.ax * (this.vx > 0 ? 1 : -1);
    this.vy += this.ay * (this.vy > 0 ? 1 : -1);
  },

Voraussetzung wäre allerdings, dass alle grafischen Objekte je einen Positionsvektor $p = (x,y)$, einen Geschwindigkeitsvektor $v = (v_x, v_y)$ sowie einen Beschleunigungsvektor $a = (a_x, a_y)$ besitzen würden.

Selbst die obige Definition der Funktion move ist noch nicht DRY. Beispielsweise sollte man an Stelle von

this.x += this.vx/FPS;
this.y += this.vy/FPS;

besser mit echten 2D-Vektor-Objekten (wie z.B. this.position und this.velocity) arbeiten, für die es Methoden wie modifyAdd und scalar gibt:

  • modifyAdd: Modifiziert den aktuellen Vektor, indem sie einen zweiten Vektor hinzuaddiert.
  • scalar: Berechnet ein Skalarprodukt, d.h multipliziert den aktuellen Vektor mit einer reelen Zahl, modifiziert aber den aktuellen Vektor nicht.

Die neue Position eines grafischen Objektes würde dann folgendermaßen ermittelt werden:

this.position.modifyAdd(this.velocity.scalar(1/FPS));

Verstöße gegen das „Gesetz von Demeter

Das Gesetz von Demeter verlangt, dass es keine Ketten von Methoden- und/oder Attributaufrufen gibt, dass ein Objekt also nicht mit „Freunden von Freunden“ kommuniziert, sondern nur mit seinen direkten „Freunden“. Anweisungen der Art object.attrtibut.methode(), in der zwei oder mehr Punktoperationen ($\cdots$.$\cdots$) vorkommen, sind verboten. (Ein führendes this. ist allerdings erlaubt.)

Die Beachtung des Gesetes von Demeter erhöht die Stetigkeit.

Gegen dieses Gesetz wird in MiniPongCanvas03 nicht verstoßen.

In der SVG-Version MiniPongSVG03 kommt derartiger Code allerdings in der init-Methode des Ball-Objektes vor:

p_canvas
  .circle(this.x, this.y, this.r)
  .attr({"stroke"       : BALL_BORDER_COLOR,
         "stroke-width" : BALL_BORDER_WIDTH,
         "fill"         : BALL_COLOR,
        });

In diesen speziellen Fall liegt allerdings kein Verstoß gegen das Gesetz von Demeter vor.

Die Methode circle fungiert als Konstruktor. Sie erzeugt ein neues Objekt und liefert das neu erzeugte Objekt als Ergebnis. Auf dieses Objekt wird die Methode attr angewendet. Der obige Code könnte also auch – etwas umständlicher – folgendermaßen geschrieben werden:

var l_circle = p_canvas.circle(this.x, this.y, this.r);

l_circle.attr({"stroke"       : BALL_BORDER_COLOR,
               "stroke-width" : BALL_BORDER_WIDTH,
               "fill"         : BALL_COLOR,
              });

Das heißt, bei beiden der obigen Methodenaufrufen „spricht“ das Ball-Objekt nur, wie von Demeter gefordert, mit einem ihm direkt bekannten „Freund“: p_canvas wurde ihm als Parameter übergeben und l_circle hat es selbst erzeugt.

Verstöße gegen das „Prinzip der Überprüfbarkeit

Bis auf ein einfaches Use-Cases-Diagramm und ein Objektmodell gibt es derzeit keine formale Spezifikation der Anwendung.

Das heißt: Derzeit kann formal nicht beweisen werden, dass die Anwendung das tut, was von ihr erwartet wird. Es gibt auch keine Testfälle, mit denen die Erfüllung der Spezifikation zumindest teilweise verifiziert werden könnte. Es bleibt dem Entwickler nur, Beta-Tester zu suchen, die das Spiel ausführlich auf diversen Brwosern testen und ihm gefundene Probleme mitteilen. (Er selbst kann auch als Beta-Tester fungieren.)

Es fehlen:

  • Ein gutes (UML-)Datenmodell, das möglichst viele Aspekte der guten Programmierung umsetzt.
  • Ein Code-Generator, der das Datenmodell automatisch in ein Codegerüst überführt.
  • Eine formale Spezifikation der Aufgaben der Funktionen und Methoden oder zumindest eine hinreichende Anzahl an Testfällen (unit tests), mit der die Semantik der Funktionen und Methoden beschrieben wird.
  • Ein Testwerkzeug, wie z.B. Buster,js, um zu verifizieren, ob der aktuelle Code tatsächlich die mit Hilfe von Testfällen spezifizierte Semantik realisiert. (Siehe auch Liste von Unit-Werkzeugen auf Wikipedia)

Verstöße gegen das Prinzip „Benzutze Interfaces

In JavaScript gibt es keine Klassen und damit erst recht keine Interfaces (= Klasse ohne Implementierung der Methoden). Daher kann dieses Prinzip in JavaScript-Code höchstens auf Spezifikationsebene (d.h. in Objekt- und Klassendiagrammen), aber nicht im Code selbst beachtet werden.

Verstöße gegen das Prinzip „Design by Contract

Da es für die MiniPong-Anwendung keinerlei formale Spezifikation gibt (zu der auch die Definition von Integritätsbedigungen zählt), verletzt diese Anwendung das Prinzip „Design by Contract“ zu 100 Prozent.

Das Prinzip „Design by Contract“ wurde von Bertrand Meyer formuliert.[1] Es besagt im Wesentlichen, dass jede Operation (Funktion, Prozedur, Methode ...) einen „Vertrag“ zu erfüllen hat: Wenn der Aufrufer sicherstellt, dass die so genannten Vorbedingungen erfüllt sind, führt die Operation ihre Aufgabe ordnungsgemäß aus. Das heißt, sie garantiert, dass durch die Ausführung des zugehörigen Codes keine Invarianten verletzt werden und dass nach Abschluss der Operation alle Nachbedingungen erfüllt sind.

Für jede Operation müssen also so genannte Integritätsbedingungen definiert werden:

  • Vorbedingungen müssen erfüllt sein, bevor eine Operation ausgeführt werden kann.
  • Nachbedingungen müssen erfüllt sein, nachdem eine Operation ausgeführt wurde.
  • Invarianten müssten stets erfüllt sein.

Bei JavaScript gibt es noch nicht einmal die einfachste Form der Integritätsbedingungen, die Typisierung von Variablen und Parametern. Jede Variable und jeder Parameter kann zu jedem Zeitpunkt einen Wert oder ein Objekt von einem anderen Typ haben. Es ist z.B. jederzeit möglich, der Variablen x des Ball-Objekts eine Zeichenkette als Wert zuzuweisen und damit einen Laufzeitfehler zu provozieren:

g_ball.x = "ganz links am Bildschirmrand";

Um dieses Problem abzumildern, muss man entweder

  • geeignete Sicherheitsabfragen (z.B. mit Hilfe von Assertbefehlen wie assert(isInteger(x))) in den Code einfügen oder
  • zahlreiche zusätzliche Testfälle generieren, die in einer typisierten Sprache überflüssig wären, oder
  • ein Werkzeug verwenden, dass die Korrektheit einer JavaScript-Anwendung automatisch beweist oder widerlegt.

Die erste Lösung hat zwei gravierende Nachteile:

  • Fehler werden erst zur Laufzeit und nicht zur Entwicklungszeit (Compilezeit) entdeckt.
  • Assert-Befehle verlangsamen den Code, während Integritätsbedingungen, die von einem Compiler zur Compilezeit berücksichtigt werden, den Code sogar beschleunigen können.

Die zweite Lösung ist machbar, aber ziemlich aufwändig, wenn man alle Testfälle „von Hand“ generieren muss.

Die dritte Lösung wäre wunderbar geeignet, aber leider gibt es keine bislang keine derartigen Werkzeuge für JavaScript. Zumindest ist mir (Wolfgang Kowarschick) keines bekannt.

Für die erste Lösung des Problems bietet sich eine Kompromisslösung an. Man versieht den Code mit Assertbefehlen und anderen Integritätstests und lässt diese später beim Komprimieren des JavaScript-Codes automatisch entfernen. So bekommt man zum Entwicklungszeitpunkt zumindest bei Laufzeittests aussagekräftige Fehlermeldungen, hat aber im Produktivbetrieb keine Leistungseinbußen.

Verstöße gegen das „Liskovsches Substitutionsprinzip

Das „Liskovsche Substitutionsprinzip“ besagt, dass man eine geerbte Methode nicht so überschreiben darf, dass sich das Objekt überraschend anders verhält, als es es täte, wenn die Methode nicht überschrieben worden wäre.

Da Vererbung in MiniPong gar nicht verwendet wird, wird gegen dieses Prinzip auch nicht verstoßen.

Verstöße gegen das „Prinzip der Modularität

MiniPongCanvas03 wurde als „Moloch“ realisiert. Von Modularisierung ist allenfalls am Rande etwas zu merken:

  • Die Konstanten wurden in eine eigene Datei ausgelagert (aber liegen immer noch direkt im ROOT-Objekt der Anwendung).
  • Die Ball- und Paddle-Methoden wurden in die das Ball- und das Schläger-Objekt integriert.

Hier besteht daher noch großes Verbesserungspotenzial. Es gibt immer noch viele zu viele globale Variablen (g_...) und zu viele globale Funktionen (f_... sowie alle Observer-Funktionen). Bei sauberer Modularisierung des Codes lösen sich viele der zuvor beschriebenen Probleme in Wohlgefallen auf.

Anmerkung: Verglichen mit einer früheren Version von MiniPongCanvas03 ist der aktuelle Code allerdings schon richtig modular. Man sehe sich mal main.js der Vorgänger-Version vom Wintersemester 2011 an. Hier ist sämlicher Code in einer Datei enthalten (die Konstanten sind im Code verteilt), ein Ball-Objekt und ein Schläger-Objekt gibt es auch noch nicht.

Quellen

  1. 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 Teil 4 des Tutoriums bearbeiten.