HTML5-Tutorium: Canvas: MiniPong 01

aus GlossarWiki, der Glossar-Datenbank der Fachhochschule Augsburg

Dieser Artikel erfüllt die GlossarWiki-Qualitätsanforderungen nur teilweise:

Korrektheit: 3
(zu größeren Teilen überprüft)
Umfang: 3
(einige wichtige Fakten fehlen)
Quellenangaben: 5
(vollständig vorhanden)
Quellenarten: 5
(ausgezeichnet)
Konformität: 5
(ausgezeichnet)

HTML-Tutorium: MiniPong

MiniPong: | Teil 1 | Teil 2 | Teil 3 | Teil 4 | Teil 5

Musterlösung: MiniPong 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

Klassendiagramm

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 Radius r, 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 und draw 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 von main.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-Datei init.json lädt und zu guter Letzt die Initialisierungsfunktion der eigentlichen Anwendung ausführt.
  • index.html: Eine HTML-Datei mit leeren Body-Element, die main.css, RequireJS und zu guter Letzt main.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 das model-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 des move-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 Attributewidth“ (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:

  1. Bewegen des Balls an seine neue Position.
  2. Kollisionserkennung und -behandlung: Kollidiert der Ball mit einer Wand?
  3. 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).
  4. 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

  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)
  3. Musterlösung MiniPong 01 (SVN)
  4. Musterlösung MiniPong 01a (SVN)

Fortsetzung des Tutoriums

Sie sollten nun Teil 2 des Tutoriums bearbeiten.