HTML5-Tutorium: Canvas: MiniPong 05
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)
Dieser Artikel wird derzeit von einem Autor gründlich bearbeitet. Die Inhalte sind daher evtl. noch inkonsistent.
Ziel: Simulation von Klassen in JavaScript
Im vierten Teil des Tutoriums wird beschrieben, wie man Klassen in JavaScript nachbilden kann.
Vier Klassen werden implementiert: Main
, Ball
, Paddle
und Collision
.
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
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.
index.html
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
und fügen Sie anschließend folgende drei Zeilen in die Datei index.html
ein (vor der Zeile, in der js/main.js
geladen wird):
<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>
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.
ball.js
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 neu erstellten Objekt (this
) Attribute erstellt und initialisiert.
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]
* 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();
};
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.
Beachten Sie bitte, 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: Auch hier 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 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
//////////////////////////////////////////////////////////////////////////////
};
Beachten Sie, dass die Getter- und Setter-Methoden für die
Attribute r
, x
, y
, vx
, vy
EcmaScript-5-konform (HTML5) definiert wurden. Das hat den Vorteil, dass man mit
der üblichen Attribut-Syntax auf diese Attribute 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
(und 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. Anschließend sollten Sie die Methoden wieder aktivieren und testweise einmal einen Log-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 mussen. Oder Sie hätten
an jeder dieser Stellen ...ball.x = ...
durch ...ball.setX(...)
ersetzen müssen.
paddle.js
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
auf 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. Dies 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 genauso definiert wie in der Klasse Ball
:
Entweder gar nicht oder mittels echten 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. Dies kann mit Hilfe echter Getter-Methoden
recht elegant realisiert werden:
/** 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
main.js
TO BE DONE
Weitere Verbesserungsmöglichkeiten
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
- ↑ vgl. Meyer (1997): Bertrand Meyer; Object-oriented Software Construction; Auflage: 2; Verlag: Prentice Hall International; ISBN: 0136291554; 1997; Quellengüte: 5 (Buch)
- 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)
- 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.