HTML5-Tutorium: Canvas: MiniPong 05: Unterschied zwischen den Versionen
Kowa (Diskussion | Beiträge) Keine Bearbeitungszusammenfassung |
Kowa (Diskussion | Beiträge) |
||
Zeile 187: | Zeile 187: | ||
{ this.v_context.beginPath(); | { this.v_context.beginPath(); | ||
this.v_context.arc(this.x, this.y, this.r, 0, 2*Math.PI, true); | 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.lineWidth = BALL_BORDER_WIDTH; | ||
this.v_context. | this.v_context.strokeStyle = BALL_BORDER_COLOR; | ||
this.v_context.fillStyle = BALL_COLOR; | this.v_context.fillStyle = BALL_COLOR; | ||
this.v_context.stroke(); | this.v_context.stroke(); | ||
this.v_context.fill(); | this.v_context.fill(); |
Version vom 12. November 2013, 12:32 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 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.strokeStyle = 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:
- 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 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.
*
* @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;
},
// 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
g_ball,
g_paddle,
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 signaled.
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 signaled.
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, da 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.
// The singleton class Main.
function Main()
{ if (Main.c_element != null)
return Main.c_element;
Main.c_element = this;
var self = this,
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
);
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();
}
//Create the main object, after the HTML page has been loaded.
window.onload = function(){ new Main(); };urce lang="javascript">
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 =
{ //----------------------------------------------------------------------------
// Methods (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.
// 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 milliseconds.
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 signaled.
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 signaled.
o_stop_paddle_moving:
function(p_event)
{ this.v_paddle.stop();
},
//----------------------------------------------------------------------------
// Methods (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 milliseconds, 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 Methods
//----------------------------------------------------------------------------
};
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.
MiniPongCanvas04
wurden wichtige Programmierprinzipien nur eingeschränkt beachtet:★★★★☆ Verständlichkeit/Lesbarkeit: sehr gut
★★★★★ Schreibbarkeit: ausgezeichnete 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: vollständig vorhanden
★★★★★ Ersetzbarkeitsprinzip: wurde beachtet
★★★☆☆ Modularität: modular (die wesentlichsten Regeln wurden beachet)
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.