HTML5-Tutorium: Canvas: MiniPong 01
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 01
,
Penetration
,
Tunneling
(SVN-Repository)
Ergänzungsaufgabe: MiniPong 1a
(SVN-Repository)
Voraussetzung
Sie sollten die Inhalte des Tutoriums HTML5-Tutorium: Canvas: Hello World kennen.
Ziel: Animation des Balls
Im ersten Teil des Tutoriums wird beschrieben, wie man eine einfache Ball-Animation mit Hilfe eines Canvas-Elements erstellt. Diese Element gibt es seit HTML5. Es dient dazu dynamisch Pixelgrafiken zu erzeugen und im zugehörigen HTML-Dokument darzustellen.
Das Spiel besteht aus insgesamt vier Modulen:
ball.js
: Hier wird die JavaScript-Klasse „Ball
“ definiert. Es hat einen Radiusr
, eine Position auf der Bühne (x
,y
), einen Geschwindigkeits-Vektor (code>vx,vy
) sowie drei Attribute, die sein Aussehen definieren. Darüber hinaus gibt es zwei Methoden:move
bewegt den Ball an eine neue Position unddraw
zeichnet den Ball in den 2D-Context eines Canvas-Elements.collision.js
: Hier wird die Funktion „collision
“ definiert, die überprüft, ob ein Ball mit einer der vier „Bühnenwände“ kollidiert. Falls dies der Fall ist, wird die Bewegungsrichtung des Balls geändert (er „prallt“ an der „Wand“ ab).minipong.js
: Die Datei enthält die eigentliche Spiellogik. Es wird ein Ball erzeugt. Eine Zeichenfunktion „draw
“ ist dann dafür zuständig, die Position des Balles mehrmals pro Sekunde neu zu berechnen und den Canvas, der als Bühne fungiert, entsprechend zu aktualisieren. Zu diesem Zweck wird ein Timer gestartet, der die Draw-Methode des Spiels mit einer bestimmten Framerate aufruft.main.js
: Die Datei hat wie immer die Aufgabe, eine JSON-Datei mit Initialwerten zu laden und anschließenden das Spiel zu starten. In der JSON-Datei werden die Größe des Canvas, die Framerate sowie die Eigenschaften des Balls festgelegt. Die Datei „main.js
“ enthält keine Funktions- oder Klassendefinitionen, sondern nur Funktionsaufrufe. Daher taucht sie im Klassendiagramm nicht auf.
Im Klassendiagramm gibt es noch eine weitere Klasse namens „Stage
“. Objekte dieser Klasse beschreiben die Bühne.
Für die Kollisionsberechnung ist es nur wichtig, die Breite (width
) und die Höhe (height
) der Bühne zu kennen.
Da als Bühne ein Canvas-Objekt zum Einsatz kommt, welches insbesondere diese beiden Attribute bereits hat, ist es nicht notwendig,
eine eigene Klasse „Stage
“ zu implementieren. Es gibt also keine Modul „stage.js
“.
Anhand des Klassendiagramms sieht man auch, welche anderen Module ein Modul laden muss. Jedes Modul muss all diejenigen Module laden,
auf die es mit einem roten Pfeil verweist. Das heißt in diesem Fall muss das Modul „minipong
“ die Module
„collision
“ sowie „Ball
“ laden. Alle anderen Module benötigen keine weiteren Module, um ihre Aufgaben erledigen zu können.
Achtung: Im UML-Standard gibt es keine Farbmarkierung der Beziehungspfeile. Das heißt, die Diagramme sind nicht ganz standardkonform.
Jedes Modul dieser Web-Anwendung definiert genau eine Funktion, die es an den Aufrufer des Modul in einem Parameter der zugehörigen Callback-Funktion übergibt. Es gibt zwei Arten von Funktionen:
- Reguläre Prozeduren oder Funktionen, die beim Aufruf irgendwelche Aufgaben erledigen bzw. irgendwelche Berechnungen durchführen. Zumeist handelt es sich um Prozeduren ohne Ergebnis. In JavaScript wird allerdings nicht zwischen Prozeduren und Funktionen unterschieden. In beiden Fällen wird das Schlüsselwort „
function
“ verwendet. Daher wird dieses Schlüsselwort auch im Diagramm verwendet. - Konstruktorfunktionen, die mittels des New-Operators aufgerufen werden müssen, um neue Objekte zu erstellen. Konstruktorfunktionen dienen in EcmaScript 5 zur Definition von Klassen.
Im Klassendiagramm sind zwei Prozeduren (minipong
und collision) definiert sowie zwei Klassen, d. h. zwei Konstruktorfunktionen
(Ball
und Stage). Wenn ein Modul eine Prozedur oder eine Funktion benötigt, die in einem anderen Modul definiert ist, muss es dieses
Modul laden. Allerdings muss nicht jedes Modul, das auf ein Objekt einer bestimmten Klasse zugreifen will, auch das zugehörige Klassenmodul laden.
Das Klassenmodul enthält ja nur den Konstruktor für Objekte einer bestimmten Klasse. Jedes Objekt muss lediglich einmal erzeugt werden und kann dann
an andere Funktionen oder Objekte weitergereicht werden. Im Klassendiagramm erzeugt das Modul „minipong
“ ein Stage
-Objekt und reicht
es später an die Prozedur „collision
“ weiter. Als Stage
-Objekt wird das Canvas-Element des HTML-Dokuments verwendet.
Dieses Objekt wurde bereits beim Laden der zugehörigen HTML-Datei erzeugt und kann daher einfach minipong
aus dem DOM-Baum des
HTML-Edokument extrahiert werden. Es muss nicht noch einmal erzeugt werden.
Ein neues Projekt anlegen
Legen Sie ein neues Projekt mit dem Namen „MiniPong01
“ an.
Fügen Sie die Ordner und Dateien, die Sie im Zip-Archiv WKempty_RequireJS.zip
finden. ein. In diesem Zip-Archiv ist ein leere Projekt enthalten, das im Prinzip wie die App5 im Tutorium
HTML5-Tutorium: JavaScript: Hello World 04 aufgebaut ist (Ein paar Datei- und Ordnernamen unterscheiden sich).
Dieses Archiv enthält folgende Dateien:
web/css/main.css
: Eine leere CSS-Datei.web/json/init.json
: Eine leere JSON-Datei, in die Initialisierungsinformationen der Anwendung eingefügt werden können.web/js/lib/require
: Die Bibliothek RequireJS samt zugehörigem JSON-Plugin.web/js/app/module.js
: Ein leeres RequireJS-Modul. Ein Modul mit dem Namen „module
“ wird üblicherweise nicht benötigt. Die Datei kann allerdings als Template für die Erstellung anderer Module verwendet werden.web/js/app/game.js
: Ein RequireJS-Modul, das eine (bislang noch leere und damit funktionslose) Funktion namens „game
“ enthält. Diese Methode wird vonmain.js
aufgerufen. Ihr werden üblicherweise das Window-Objekt des Browsers sowie ein JSON-Initialisierungsobjekt übergeben.web/js/main.js
: Eine JavaScript-Datei, die RequireJS initialisiert, die JSON-Dateiinit.json
lädt und zu guter Letzt die Initialisierungsfunktion der eigentlichen Anwendung ausführt.index.html
: Eine HTML-Datei mit leeren Body-Element, diemain.css
, RequireJS und zu guter Letztmain.js
lädt.
Nennen Sie die Datei „game.js
“ in „minipong.js
“ um und ändern Sie in der Datei „main.js
“ die Zeichenkette
„game
“ an allen drei Stellen in „minipong
“ ab. Wirklich notwendig wären diese Änderungen nicht, aber ein sprechender Name
wie „minipong
“ ist verständlicher/einprägsamer als ein generischer Name wie „game
“.
Wenn Sie jetzt Ihre Anwendung (index.html
) starten, sollte der Browser eine leere Seite anzeigen. Das is OK, denn in der HTML-Datei
ist bislang nur eine leerer Body enthalten. Wichtig ist, dass im HTML-Konsolfenster, das sie erst noch öffnen müssen, kein Fehler gemeldet wird.
Anderenfalls haben Sie bei der Umbenennungsaktion irgendetwas falsch gemacht.
Die HTML-Datei
Die HTML-Datei „index.html
“ ist schon fast vollständig. Es fehlt nur noch das Canvas-Element
im Body-Element vor dem Script-Element (vgl. HTML5-Tutorium: Canvas: Hello World 02):
<canvas id="canvas"></canvas>
Außerdem sollten Sie im Header-Bereich den Dokumenttitel „TITLE
“ durch „MiniPong01
“ ersetzen.
Die CSS-Datei
Die CSS-Datei „main.css
“ des Projekts sieht folgendermaßen aus:
body
{ text-align: center;
padding: 10px;
background-color: #AAA;
}
#canvas
{ border-color: #777;
border-width: 2px;
border-style: solid;
background-color: #CCC;
}
Sie rückt das Canvas-Element in die obere Mitte der Bühne und legt die Hintergrundfarben des Browserfensters sowie des Canvas-Elements fest. Außerdem sorgt sie dafür, das ein Rand um das Canvas-Element gezeichnet wird.
Die JSON-Datei
Die JSON-Datei „init.json
“ des Projekts sieht folgendermaßen aus:
{
"canvas":
{
"width": 400,
"height": 300
},
"model":
{
"ball":
{
"r": 10,
"pos": { "x": { "min": 10, "max": 390 },
"y": { "min": 10, "max": 290 }
},
"vel": { "x": { "min": 50, "max": 200 },
"y": { "min": 50, "max": 200 }
}
}
},
"view":
{
"ball":
{
"color": "#55AA55",
"borderWidth": 2,
"borderColor": "#000000"
}
},
"game":
{
"fps": 60
}
}
Sie enthält vier Objekte zur Initialisierung der Anwendung:
canvas
: Dieses Objekt legt diejenigen Eigenschaften des Canvas-Elements fest, die zur Laufzeit mit Hilfe von JavaScript dem Element zugeordnet werden. Das sind in diesem Fall nur die Breite und die Höhe. Die Hintergrundfarbe sowie der Rand wurden mit Hilfe von CSS definiert.model
: Hier werden für jede Art von Objekten, die in dem Spiel vorkommt, physikalische Eigenschaften festgelegt, auf die die Physics-Engine zugreifen kann. Das es in unserem Spiel nur eine Objektart gibt, den Ball, enthält dasmodel
-Objekt auch nur die Beschreibung der Ballobjekte. Es legt fest, dass ein Ball stets einen festen Radius (r
) haben soll sowie eine Position (pos = “position”) und eine Geschwindigkeit (vel = “velocity”). Die Position wird durch eine $x$- und eine $y$-Koordinate relativ zur linken oberen Ecke, d. h. zum Nullpunkt der Bühne festgelegt, die Geschwindigkeit durch einen Vektor, der die Geschwindigkeiten in $x$- und in $y$-Richtung enthält. Für Position und Geschwindigkeit sind Intervalle angegeben, innerhalb derer diese Werte zufällig gewählt werden sollen. Das heißt, bei jedem Spielstart startet der Ball an einer anderen Position und bewegt sich mit einer anderen Geschwindigkeit.view
: Hier werden für jedes Objekt, das im Spiel vorkommt und auf der Bühne gezeichnet werden soll, grafische Eigenschaften festgelegt. Auch hier gibt es bislang nur das Ballobjekt. Von diesem Objekt werden die Farbe, die Breite des Randes sowie die Farbe des Randes festgelegt.game
: Diese Objekt enthält weitere spielspezifische Eigenschaften. Für dieses Spiel fehlt nur noch die Angabe der Framerate „fps
“ (= frames per seconds = Bilder pro Sekunde), d. h. die Anzahl der Neuberechnungen, die pro Sekunde vorgenommen werden sollen, um physikalische Veränderungen (Bewegung des Balls) zu simulieren. Bei jeder Änderung der Ballposition wird der Inhalt der Bühne (also ein “frame”) neu gezeichnet.
Das Ball-Modul
Das Ballmodul „ball.js
“ definiert die JavaScript-Klasse „Ball
“. Die Konstruktorfunktion
„Ball
“ erwartet als Eingabe sowohl die Modell-Daten als auch die View-Daten, die in der JSON-Datei für Ballobjekte festgelegt wurden.
Erstellen Sie zunächst eine Kopie der Datei „js/app/module.js
“ unter dem Namen „js/app/ball.js
“.
Ersetzen Sie in der neu erstellten Datei zunächst die Return-Anweisung durch folgende Anweisung:
return Ball;
Anschließend ersetzen Sie die drei Zeilen
//var module = ...;
function module()
{}
durch folgende Definition der Konstruktorfunktion „Ball
“:
function Ball(p_init_model, p_init_view)
{
// model
var l_pos = p_init_model.pos,
l_vel = p_init_model.vel;
this.r = p_init_model.r;
this.x = (l_pos.x.min + Math.random()*(l_pos.x.max - l_pos.x.min));
this.y = (l_pos.y.min + Math.random()*(l_pos.y.max - l_pos.y.min));
this.vx = (Math.random() < 0.5 ? 1 : -1)*
(l_vel.x.min + Math.random()*(l_vel.x.max - l_vel.x.min));
this.vy = (Math.random() < 0.5 ? 1 : -1)*
(l_vel.y.min + Math.random()*(l_vel.y.max - l_vel.y.min));
// view
this.color = p_init_view.color;
this.borderWidth = p_init_view.borderWidth;
this.borderColor = p_init_view.borderColor;
}
Dieser Konstruktor initialisiert zunächst die Model-Daten des Balles mit sinnvollen Werten:
r
(Radius des Balls)x
(Aktuelle x-Position des Balls)y
(Aktuelle y-Position des Balls)vx
(Aktuelle Geschwindigkeit des Balls in x-Richtung in Pixeln/Sekunde)vy
(Aktuelle Geschwindigkeit des Balls in y-Richtung in Pixeln/Sekunde)
Den Radius wird direkt aus dem Initialisierungsobjekt „p_init_model
“ gelsen, die aktuelle Position und die aktuelle Geschwindigkeit
berechnet der Konstruktor mit Hilfe des Zufallszahlengenerators „Math.random
“ so, dass die Werte in den Intervallen liegen, die in
p_init_model
angegeben wurden. Der Aufruf „Math.random()
“ liefert eine Float-Zahl, die größer oder gleich 0 ist und kleiner als 1.
Bei der Geschwindigkeit wir mittels Math.random
auch noch das Vorzeichen bestimmt. Der
Methodenaufruf „(Math.random() < 0.5 ? 1 : -1)
“ liefert in 50% der Fälle eine 1 als Ergebnis und in den restlichen 50% der Fälle eine -1.
Im Falle der x-Geschwindigkeit bedeutet das, dass sich der Ball in 50% der Fälle nach links und in 50% der Fälle nach rechts bewegt.
Im Fall der y-Geschwindigkeit wird auf dieselbe Weise zufällig ermittelt, ob sich der Ball zu Beginn nach unten oder nach oben bewegen soll.
Der Konstruktor initialisiert auch die View-Daten des Balles: color
, borderWidth
und borderColor
.
Die entsprechenden Werte übernimmt er einfach dem Initialisierungsobjekt „p_init_view
“.
Die Klasse „Ball
“ definiert für jedes Ballobjekt auch noch zwei Methoden. Wie üblich werden diese Methoden
im Prototyp-Objekt „Ball.prototype
“ des Konstruktors gespeichert, damit nicht für jedes Ballobjekt eine eigen Methode erzeugt wird.
move
bewegt den Ball an seine neue Position; diese wird aus der aktuellen Position, dem Geschwindigkeitsvektor und der Zeitspanne, die seit dem letzten Aufruf desmove
-Befehls vergangen ist, berechnet.draw
visualisiert das (zweidimensionale) Ballobjekt auf dem 2D-Kontext einer Bühne (Canvas).
Fügen Sie folgende Definitionen in die Datei „ball.js
“
direkt hinter die Konstruktorfunktion „Ball
“ ein:
Ball.prototype =
{
/**
* Moves the ball to a new position. The new position depends on its
* current position, its velocity and the model update rate.
*
* @param p_seconds Fraction of seconds since the last update.
*/
move:
function(p_seconds)
{
this.x += this.vx * p_seconds;
this.y += this.vy * p_seconds;
},
/**
* Draws the ball at its current position onto a 2d context.
* As a ball may be drawn on several stages, the context
* is passed via a parameter.
*
* @param p_context The 2d context where the ball is to be drawn.
*/
draw:
function(p_context)
{
p_context.beginPath();
p_context.arc(this.x, this.y, this.r, 0, 2 * Math.PI);
p_context.fillStyle = this.color;
if (this.borderWidth > 0)
{
p_context.lineWidth = this.borderWidth;
p_context.strokeStyle = this.borderColor;
p_context.stroke(); // Draw the border.
}
p_context.fill(); // Fill the inner area of the ball with its color.
}
};
Die Methode „move
“ erwartet als Eingabe die Anzahl der Sekunden, die seit dem letzten Aufruf dieser Methode vergangen sind.
Wenn die Framerate fps
beträgt, wenn also die Move-Methode fps
mal pro Sekunde aufgerufen wird,
dann vergehen zwischen zwei Aufrufen 1/fps
Sekunden. Da in der JSON-Datei eine Framerate von 60 Bildern pro Sekunde
festgelegt wurde, vergehen zwischen zwei Aufrufen jeweils $1/60 = 0,01\bar{6}$ Sekunden. Als Argument für den Parameter
„p_seconds
“ muss der Methode „move
“ daher der Wert 0,0166666666667
übergeben werden.
Wenn sich der Ball in einer Sekunde um vx
Pixel in x-Richtung und um vy
Pixel in y-Richtung bewegt,
dann bewegt er sich in $0,01\bar{6}$ Sekunden um vx * 0,0166666666667
(=== this.vx * p_seconds
)
Pixel in x-Richtung und um vy * 0,0166666666667
(=== this.vy * p_seconds
) Pixel in y-Richtung. Diese Beträge müssen
also zu den $x$- und $y$-Koordinaten der aktuellen Ballposition addiert werden.
Die Methode „draw
“ wird ebenfalls alle paar Millisekunden aufgerufen. Wie häufig das passiert ist allerdings unwesentlich.
Ihre Aufgabe ist nur, dass sie jedes mal, wenn sie aufgerufen wird, den Ball an seiner aktuellen Postion auf der Bühne zeichnet und
dabei natürlich die Farben und die Breite des Randes berücksichtigt, die dem Ballobjekt zugeordnet sind.
Sie verwendet dazu Befehle des 2D-Kontextes eines Canvas-Elements. Daher muss ihr as 2D-Kontext-Objekt des Canvas-Elements
als Argument übergeben werden.
Zunächst definiert die Methode im aktuellen 2D-Kontext ein neues Pfad-Objekt. Als Pfad definiert sie einen Kreisbogen, der bei 0
startet und bei 2*Math.PI
(entspricht 360°) endet. Das heißt. bei dem Pfad handelt es sich um einen geschlossenen Kreis.
Anschließend legt sie die Breite des Pfades, die Farbe des Pfades sowie die Füllfarbe des Gebietes, das der Pfad umschließt fest. Dazu greift sie auf die
entsprechenden Werte des Ballobjektes zurück. Danach zeichnet sie – sofern der Rand sichtbar, also dessen Breite größer als Null ist –
den Pfad auf die Bühne (p_context.stroke()
). Zu guter Letzt füllt sie den vom Pfad umschlossenen Innenraum mit Farbe (p_context.fill()
).
Das Kollisions-Modul
In der Datei „collision.js
“ wird für jedes bewegliche Objekt überprüft, ob es mit irgendeinem anderen
(beweglichen oder unbeweglichen) Objekt kollidiert. Sollte dies der Fall sein, wird die Kollision behandelt,
das heißt, es werden Eigenschaften des Objektes geändert. Es könnte beispielsweise seine Richtung und Geschwindigkeit ändern,
sein Aussehen ändern, explodieren oder oder oder.
In unserem Fall gibt es nur ein bewegliches Objekt, den Ball. Dieser kann nur mit unbeweglichen Objekten kollidieren: Den „Bühnenwänden“. Als Reaktion auf eine derartige Kollision prallt der Ball ab, d. h. er ändert seine Richtung. Das ist bei Kollisionen mit senkrechten oder waagerechten Wänden ganz einfach: Es muss jeweils nur das Vorzeichen der $x$- bzw. der $y$-Geschwindigkeit geändert werden.
Dementsprechend wird im Kollisionsmodul nur eine Funktion namens „collision
“ definiert und als Ergebnis zurückgegeben.
Diese Funktion erhält als Input die Bühne, welches zwei Attribute „width
“ (Breite der Bühne) und „height
“
(Höhe der Bühne) enthalten muss, sowie das Ballobjekt, das mit den Bühnenwänden kollidieren kann. Als Bühne fungiert in der MiniPong-Anwendung das
Canvas-Element, das Sie in HTML-Datei „index.html
“ eingefügt haben. Jedes Canvas-Objekt enthält die beiden benötigten Attribute
„width
“ und „height
“.
Erstellen Sie das Kollisions-Modul auf die gleiche Weise wie das Ball-Modul. Dieses Modul soll folgende Funktion definieren:
function collision(p_stage, p_ball)
{
// If the ball collides with the left or the right wall of the stage
// mirror its x-velocity.
if (p_ball.x <= p_ball.r || p_ball.x + p_ball.r >= p_stage.width)
{ p_ball.vx = -p_ball.vx; }
// If the ball collides with the top or the bottom wall of the stage
// mirror its y-velocity.
if (p_ball.y <= p_ball.r || p_ball.y + p_ball.r >= p_stage.height)
{ p_ball.vy = -p_ball.vy; }
}
Das MiniPong-Modul
Das MiniPong-Modul ist für die Spiele-Logik zuständig. Das Spiel ist ziemlich einfach. Es wird ein Ball erstellt, der sich mit einer gewissen Geschwindigkeit über die Bühne bewegt.
Wie dieser Ball aussieht und welche Eigenschaften er hat, wird dem Konstruktor ebenso wie die Größe der Bühnen und die Framerate
im JSON-Objekt „p_init
“ übergeben. Die Bühne selbst wird im Parameter „p_context
“ übermittelt.
Zunächst werden der Canvas und der zugehörige 2D-Kontext aus dem HTML-Dokument extrahiert und ein Ballobjekt erstellt.
Außerdem werden aus der Framerate die Anzahl der Sekunden und die Anzahl der Millisekunden berechnet, die zwischen zwei Aufrufen der
Ball-Methoden „move
“ und „draw
“ vergehen (bzw. vergehen würden, sofern der Browser einen echtzeitfähigen Timer
zur Verfügung stellen würde). Da im HTML-Dokument für das Canvas-Element keine Breite und keine Höhe angegeben wurde, hat es die Defaultmaße.
Im JSON-Objekt wurden die gewünschten Maße für dieses Element festgelegt. Daher muss im Anschluss an die Variablendefinitionen
zunächst die Größe des Canvas-Elements aktualisiert werden. Insgesamt ergibt sich folgender Code:
function MiniPong(p_init, p_context)
{
var l_canvas_init = p_init.canvas, // contains width and height of the canvas
l_canvas = p_window.document.getElementById("canvas"),
l_context = l_canvas.getContext("2d"),
l_ball = new Ball(p_init.model.ball, p_init.view.ball),
l_seconds = 1 / p_init.game.fps,
l_milliseconds = 1000 * l_seconds;
l_canvas.width = l_canvas_init.width;
l_canvas.height = l_canvas_init.height;
...
}
Aus p_init
wurden das Modell-Initialisierungsobjekt „p_init.model.ball
“
und das View-Initialisierungsobjekt „p_init.view.ball
“ extrahiert und dem Ball-Konstruktor übergeben.
Damit hat man einen Ball, der sich über die Bühne bewegen kann. Zu diesem Zweck gibt es
der Funktion „draw
“. Das ist die Kernfunktion des MiniPong-Spiels.
Diese Funktion führt bei jedem Aufruf folgende Aktionen durch:
- Bewegen des Balls an seine neue Position.
- Kollisionserkennung und -behandlung: Kollidiert der Ball mit einer Wand?
- Löschen aller Inhalte der Bühne, damit sie neu gezeichnet werden kann. (Kommentieren Sie diesen Befehl testhalber einmal aus, sobald Ihre Anwendung läuft).
- Zeichnen des Balls (an der zuvor berechneten Position) auf die Bühne.
Man beachte, dass die Kollisionserkennung und -Behandlung erfolgt, nachdem der Ball an eine neue Position bewegt wurde. Hier kommt also die sogenannte A-posteriori-Kollisionserkennung und -behandlung zun Einsatz. Diese ist einfach handzuhaben, führt aber häufig zu unerwünschten Tunneling- oder Penetrationseffekten (siehe Abschnitt Probleme der Kollisionsbehandlung).
Die Draw-Methode wird innerhalb der Funktion „minipong
“ definiert.
Dies hat den Vorteil, dass sie auf die lokalen Variablen „l_ball
“, „l_seconds
“
etc. zugreifen kann, die ebenfalls innerhalb dieser Funktion definiert wurden und auch nur dort zugänglich sind.
Von außen kann auf diese Variablen nicht zugegriffen werden. Direkt im Anschluss an die Funktionsdefinition wird
ein Timer-Objekt erstellt, das die Draw-Funktion alle l_milliseconds
aufruft. Dazu wird die
JavaScript-Funktion „setInterval
“ verwendet. Diese Funktion ist als Methode im Window-Objekt
der Web-Anwendung enthalten. Allerdings ist sie relativ ungenau. Sie bemüht sich zwar, zwischen zwei Aufrufen
der Draw-Funktion jeweils l_milliseconds
Millisekunden vergehen zu lassen, aber das klappt nur
dann einigermaßen, wenn das Betriebssystem oder der Browser nicht gerade anderweitig beschäftigt ist.
Bei einem gut ausgelasteten Rechner wird das Spiel daher öfters ruckeln. Diese Problem und mögliche Problemlösungen
sollen aber hier nicht weiter thematisiert werden.
function draw()
{
// model update
l_ball.move(l_seconds);
// a posteriori collision detection and handling
collision(l_canvas, l_ball);
// view update
l_context.clearRect(0, 0, l_canvas.width, l_canvas.height); // clear
l_ball.draw(l_context);
}
p_window.setInterval(draw, l_milliseconds);
Das zugehörige Modul wird abermals auf die gleiche Weise wie das Ball-Modul erstellt. Allerdings benötigt dieses Module sowohl den zuvor definierten Ball-Konstruktor, damit es einen (oder später auch mehrere) neue Bälle erzeugen kann. Außerdem benötigt es die Kollisionsfunktion, um die Richtung des Balles im Falle einer Kollision mit einer Wand anpassen zu können.
Insgesamt muss das Modul also zunächst die beiden Module
„app/ball
“ und „app/collision
“
laden und die geladenen Objekt als Funktionsargumente der Callback-Funktion übergeben.
Der Modul-Header sieht somit folgendermaßen aus:
define
(['app/ball', 'app/collision'],
function(Ball, collision)
...
Das Main-Modul
Das Main-Modul liest wie üblich eine JSON-Initialisierungsdatei ein und startet die Web-Anwendung.
Dieses Modul kann mit den zu Beginn genannten Änderungen aus dem leeren Projekt „WKempty_RequireJS
“ übernommen werden.
requirejs.config
({
baseUrl: 'js', // By default load any modules from directory js
paths :
{
app: 'app',
json: '../json',
loadjson: 'lib/require/json',
text: 'lib/require/text'
}
});
requirejs
( ['loadjson!json/init.json', 'app/minipong'],
function(initJSON, minipong)
{
minipong(window, initJSON);
}
);
Probleme der Kollisionsbehandlung
Die A-posteriori-Kollisionserkennung und -behandlung, die bei der Erkennung von Kollisionen zwischen Ball und Wand angewandt wurde, ist nicht ganz unproblematisch, da zwei unerwünschte Effekte auftreten können:
- Überlappung (penetration = Eindringung)
- Der Ball dringt i. Allg. etwas in die Wand ein, bevor eine Kollision erkannt wird. Wenn dieses Eindringen durch die Kollisionsbehandlung nicht rückgängig gemacht wird, kann es passieren, dass der Ball in der Wand hängen bleibt, da er sich in allen folgenden Animationsschritten in der Wand befindet und daher in jedem Schritt seine Richtung ändert.
- Tunneln (tunneling)
- Wenn der Ball besonders schnell ist, kann er ein anderes Objekt eventuell durchtunneln. Dies passiert immer dann, wenn der Ball in einem Schritt eine so große Distanz zurücklegt, dass weder am Startpunkt noch am Endpunkt dieses Schritts eine Kollision erkannt wird, obwohl entlang des Weges eine Kollision stattgefunden hätte.
Beide Effekte können Sie beobachten, wenn Sie Ihren MiniPong-Code etwas modifizieren.
- Überlappung
- Setzen Sie in der Datei
init.json
den minimalen und den maximalen $y$-Wert jeweils auf 300, d. h., platzieren Sie den Ball gleich zu Beginn in der Wand, und starten Sie die Anwendung. Nach dem Sie den Effekt bewundert haben, sollten Sie den Fehler auch wieder beheben. :-)"pos": { "x": { "min": 10, "max": 390 }, "y": { "min": 300, "max": 300 } }
- Tunneln
- Da die Wände im Kollisionstest unendlich dick gewählt wurden, kann der Ball diese nicht durchdringen. Wenn Sie aber die Breite der Wände in der Funktion
„
collision
“ (in der Datei „collision.js
“) auf Null reduzieren, haben Sie nicht lange Freude an Ihrem Ball:if (p_ball.x == p_ball.r || p_ball.x + p_ball.r == p_stage.width) { p_ball.vx = -p_ball.vx; } if (p_ball.y == p_ball.r || p_ball.y + p_ball.r == p_stage.height) { p_ball.vy = -p_ball.vy; }
Erweiterung
Erweitern Sie die Anwendung so, dass sich zwanzig Bälle gleichzeitig über die Bühne bewegen. Kollisionen zwischen den Bällen brauchen Sie nicht zu behandeln.
Beachten Sie die Änderung des Klassenmodells: Im ursprünglichen Klassenmodell ist an der Spitze des Pfeils von
der Funktion „minipong
“ zur Klasse „Ball
“ die Vielfachheit „1
“ angegeben. In diesem Diagramm steht dort die
„0..*
“. Das heißt, die Funktion „minipong
“ muss beliebig viele Bälle erzeugen und animieren können.
Wie viele Bälle das sind, sollte in der JSON-Datei festgelegt werden. Die erzeugten Bälle sollten – wie zuvor das einzelne Ballobjekt – innerhalb der Funktion „minipong
“ gespeichert werden. Als Container für die Speicherung der Bälle verwenden Sie am Besten ein Array.
Musterlösung: MiniPong 1a
(SVN-Repository)
Quellen
- 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)
- Musterlösung MiniPong 01 (SVN)
- Musterlösung MiniPong 01a (SVN)
Fortsetzung des Tutoriums
Sie sollten nun Teil 2 des Tutoriums bearbeiten.