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 1 (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.

Das Spiel besteht aus insgesamt fünf Modulen:

  • ball.js: Hier wird die JavaScript-Klasse Ball definiert.
  • collision.js: Hier wird die Funktion collision definiert, die überprüft, ob ein Ball mit einer „Wand“ des Canvas-Elements 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 entsprechend zu aktualisieren.
  • init.js: Die Aufgabe der Init-Funktion ist es, die Größe des Canvas gemäß der Initialisierungswerte, die ihr übergeben werden, festzulegen, ein MiniPong-Objekt zur erzeugen und einen Timer zu starten, der die Draw-Methode des Spiels mit einer bestimmten Framerate aufruft.
  • main.js: Die Datei hat wie immer die Aufgabe, eine JSON-Datei mit den Initialwerten zu laden und anschließenden die Initialisierungsmethode des Spiels auszuführen. In der JSON-Datei werden die Größe des Canvas, die Framerate sowie die Eigenschaften des Balls festgelegt.

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/class.js: Ein RequireJS-Modul, das eine leere Klasse namens „Class“ definiert. Eine Klasse dieses Namens wird üblicherweise nicht benötigt. Die Datei kann allerdings als Template für die Erstellung anderer Klassen verwendet werden.
  • web/js/app/init.js: Ein RequireJS-Modul, das eine (bislang noch leere und damit funktionslose) Initialisierungsfunktion enthält. Diese Methode wird von main.js aufgerufen. Ihr werden 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.

Speichern Sie dieses Projekt wie üblich in Ihrem Repository.

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>

Die CSS-Datei

Die CSS-Datei „main.css“ des Projektes 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 Projektes sieht folgendermaßen aus:

 
{
  "canvas":
  {
    "width":  400,
    "height": 300
  },

  "physics":
  {
    "framesPerSecond": 50
  },

  "model":
  {
    "ball":
    {
      "radius":  8,
      "pos_min": {"x":  10, "y":  10},
      "pos_max": {"x": 390, "y": 290},
      "vel_min": {"x":  50, "y":  50},
      "vel_max": {"x": 200, "y": 200}
    }
  },

  "view":
  {
    "ball":
    {
      "color":       "#55AA55",
      "border":      1,
      "borderColor": "#000000"
    }
  }
}

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.
  • physics: Diese Objekt enthält Eigenschaften der Physics-Engine, die für das Spiel zum Einsatz kommt. In diesem Spiel ist das nur die Framerate, d. h. die Anzahl der Neuberechnung, die pro Sekunde vorgenommen werden sollen, um physikalische Veränderungen (Bewegung des Balls) zu simulieren.
  • 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 haben soll, sowie eine Position und eine Geschwindigkeit. Für Position und Geschwindigkeit sind Intervalle angegeben innerhalb derer diese Werte zufällig gewählt werden sollen. Das heißt, bei jedem Spielstart stratet der Ball an einer anderen Position und bewegt sich mit einer anderen Geschwindigkeit.
  • model: Hier werden für jede Art von Objekten, die in dem Spiel vorkommt und das 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.

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 Ball-Objekte festgelegt wurden.

function Ball(p_init_model, p_init_view)
{
  // model
  var l_pos_min = p_init_model.pos_min,
      l_pos_max = p_init_model.pos_max,
      l_vel_min = p_init_model.vel_min,
      l_vel_max = p_init_model.vel_max;

  this.r = p_init_model.radius;

  this.x = (l_pos_min.x + Math.random()*(l_pos_max.x - l_pos_min.x));  // x in [l_pos_min.x, l_pos_max.x[
  this.y = (l_pos_min.y + Math.random()*(l_pos_max.y - l_pos_min.y));  // x in [l_pos_min.y, l_pos_max.y[

  this.vx = (Math.random() < 0.5 ? 1 : -1)*                            // vx in ]-l_vel_max.x, l_vel_max.y[ \
            (l_vel_min.x + Math.random()*(l_vel_max.x - l_vel_min.x)); //       ]-l_vel_min.x, l_vel_min.x[
  this.vy = (Math.random() < 0.5 ? 1 : -1)*                            // vx in ]-l_vel_max.y, l_vel_max.y[ \
            (l_vel_min.y + Math.random()*(l_vel_max.y - l_vel_min.y)); //       ]-l_vel_min.y, l_vel_min.y[

  // 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 Modell-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 entnimmt sie direkt dem Initialisierungsobjekt „p_init_model“, die aktuelle Position und die aktuelle Geschwindigkeit berechnet sie mit Hilfe des Zufallszahlengenerators „Math.random“ so, dass die Werte in den Intervallen liegen, die i 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“.


Darüber hinaus besitzt dieses Objekt zwei Methoden:

  • move bewegt den Ball an seine neue Position; dieses wird aus dem Geschwindigkeitsvektor und der Zeitspanne, die zwischen zwei Aufrufen des move-Befehls vergeht, berechnet
  • draw visualisiert das (zweidimensionale) Ball-Objekt auf dem 2D-Kontext einer Bühne (Canvas).
// the ball
    g_ball = 
    { r:  BALL_RADIUS,
      x:  BALL_X,
      y:  BALL_Y,
      vx:   (Math.random() < 0.5 ? 1 : -1)
          * (BALL_VX_MIN + Math.random() * (BALL_VX_MAX - BALL_VX_MIN)), 
      vy:   (Math.random() < 0.5 ? 1 : -1)
          * (BALL_VY_MIN + Math.random() * (BALL_VY_MAX - BALL_VY_MIN)),

      // 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;
        },

      // Draws the ball at its current position onto a 2d context.
      draw:
        function(p_context)
        { p_context.beginPath();
          p_context.arc(this.x, this.y, this.r, 0, 2*Math.PI);
          p_context.lineWidth   = BALL_BORDER_WIDTH;
          p_context.strokeStyle = BALL_BORDER_COLOR;
          p_context.fillStyle   = BALL_COLOR;
          p_context.stroke();
          p_context.fill();
        }
    };

Die so genannte Observer-Methode o_redraw() wird alle paar Millisekunden vom Timer aufgerufen. Diese Methode hat drei Aufgaben:

  1. Kollisionen zwischen Ball und Wand erkennen und gegebennenfalls die Bewegungsrichtung des Balls ändern.
  2. Den Ball an seine neue Position bewegen.
  3. Den aktuellen Inhalt der Bühne löschen und den Ball an der neuen Position auf die Bühne zeichnen.
// An event observer: 
// It is called every 1000/FPS milliseconds.
function o_redraw() 
{ // Collision detection: ball <-> wall 
  // Collision response:  change the moving direction of the ball
  if (g_ball.x <= g_ball.r || g_ball.x >= CANVAS_WIDTH  - g_ball.r)
  { g_ball.vx = -g_ball.vx; }
  if (g_ball.y <= g_ball.r || g_ball.y >= CANVAS_HEIGHT - g_ball.r)
  { g_ball.vy = -g_ball.vy; }
  
  // Move the ball.
  g_ball.move();

  // Clear and redraw the canvas
  g_context.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
  g_ball.draw(g_context);
}

Sobald die HTML-Seite vollständig geladen worden ist, werden die Bühne initialisiert und der Timer gestartet.

// Has to be called after the HTML page has been loaded.
function f_init() 
{ var l_canvas = document.getElementById("d_canvas");

  // Initialize the canvas.
  l_canvas.width  = CANVAS_WIDTH;
  l_canvas.height = CANVAS_HEIGHT;
  g_context       = l_canvas.getContext("2d");

  // Start the timer for redrawing the canvas every 1000/FPS milliseconds.
  g_timer = window.setInterval(o_redraw, 1000/FPS);
}

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

Dieses Skript realisiert die eigentliche Ball-Animation. Zum Einsatz kommen dabei Techniken, die im Tutorium von Alexander Lawrence genauer beschrieben werden: AS3-Tutorium:Physics. Sehen Sie sich insbesondere folgende Abschnitte an:

Anwendung testen und sichern

Testen Sie Ihre Anwendung in gewohnter Weise.

Vergessen Sie nicht, sie im SVN-Repository zu sichern.

Probleme der Kollisionserkennung

Die A-posteriori-Kollissionserkennung, die in Minipong 1 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 Kollissionsbehandlung 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-1-Code etwas modifizieren.

Überlappung
Setzen Sie BALL_Y = CANVAS_HEIGHT (in der Datei CONSTANT.js), 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. :-)
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 o_redraw (in der Datei main.js) auf Null reduzieren, haben Sie nicht lange Freude an Ihrem Ball:
  if (g_ball.x == g_ball.r || g_ball.x == CANVAS_WIDTH  - g_ball.r)
  { g_ball.vx = -g_ball.vx; }
  if (g_ball.y == g_ball.r || g_ball.y == CANVAS_HEIGHT - g_ball.r)
  { g_ball.vy = -g_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 aber das Programmierprinzip „Don't repeat yourself“ (Dry).

Das Objektmodell von MiniPong 01a

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

Varianten

Die folgenden Beispiele wurden gegenüber der zuvor beschrieben Version in zweierlei Hinsicht erweitert:

  1. Die Implementierung ist klassenbasiert und nicht objektbasiert.
  2. Die Bälle reagieren auf Mausklicks.
Canvas, externe Bibliotheken: keine
Minipong 1 (SVN-Repository)
Minipong 1a (SVN-Repository)
Canvas, externe Bibliotheken: Prototype veraltet
Minipong 1a (SVN-Repository)
Canvas, externe Bibliotheken: jQuery, jCanvas, jQueryMX (class.js) veraltet
Minipong 1a (SVN-Repository)
SVG, externe Bibliotheken: jQuery, jCanvas, jQueryMX (class.js), Raphaël veraltet
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ösungen (SVN)