HTML5-Tutorium: Canvas: MiniPong 04: Unterschied zwischen den Versionen

aus GlossarWiki, der Glossar-Datenbank der Fachhochschule Augsburg
Kowa (Diskussion | Beiträge)
Kowa (Diskussion | Beiträge)
Zeile 59: Zeile 59:
===<code>main.js</code>===
===<code>main.js</code>===


Fügen Sie die Variablen <code>g_ball</code>, <code>g_paddle</code>, <code>g_context</code>, <code>g_interval</code> und <code>g_score</code>  
Fügen Sie die Variablen <code>g_ball</code>, <code>g_paddle</code>, <code>g_context</code>, <code>g_interval</code>
der beiden Dateien <code>main.js</code> von [[HTML5-Tutorium: Canvas: MiniPong 01#main.js|Teil 1]]
der beiden Dateien <code>main.js</code> von [[HTML5-Tutorium: Canvas: MiniPong 01#main.js|Teil 1]]
und [[HTML5-Tutorium: Canvas: MiniPong 02#main.js|Teil 2]] des Tutoriums in die Datei <code>main.js</code> ds dritten Teils ein.
und [[HTML5-Tutorium: Canvas: MiniPong 02#main.js|Teil 2]] des Tutoriums in die Datei <code>main.js</code> des dritten Teils ein.
Definieren Sie zusätzlich eine globale Variable <code>g_score</code>, in der der aktuelle Punktestand des Spiels abgelegt wird.  


'''Achtung: Definieren Sie keine Variable doppelt.'''
'''Achtung: Definieren Sie keine Variable doppelt.'''

Version vom 18. Oktober 2012, 16:31 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 3 (SVN-Repository)

Ziel

Im dritten Teil des Tutoriums wird beschrieben, wie man MiniPong mit Hilfe einer einzigen JavaScript-Datei erstellt. Wolfgang Kowarschick bezeichnet so eine Datei als „Moloch-Datei“ (Moloch = Monstrum, Ungeheuer, alles verschlingende Bestie).

Neues Projekt anlegen

Legen Sie ein neues Projekt MiniPong02 an und speichern Sie dieses Projekt auf die bekannte Weise in Ihrem SVN-Repository.

Dateien erstellen

Kopieren Sie die Dateien index.html und css/Main.css von Teil 2 des Tutoriums, passen Sie den Projekttitel in der Datei index.html an und erstellen Sie die Dateien js/CONSTANT.js js/Main.js neu.

index.html

Fügen Sie in den HTML-Rumpf (body) folgenden Code vor das canvas-Element ein:

<p id="d_info">&nbsp;</p>

Der Text dieses Elements wird von JavaScript aus geändert, um den jeweiligen Punktestand anzuzeigen. &nbsp; ist ein Leerzeichen und sorgt dafür, dass sich Eclipse nicht über ein leeres p-Element beschwert.

Fügen Sie in den HTML-Rumpf (body) folgendes Formular hinter das canvas-Element ein:

<form>
  <input id="d_start_stop" type="button"/>
</form>

Dieser Button fungiert – je nach aktuellen Status des Spiels – als Start- oder Stopp-Button. Die Beschriftung sowie die Zuordnung der zugehörigen Aktion wird jeweils mittels JavaScript an den aktuellen Zustand der Anwendung angepasst.

CONSTANT.js

Fügen Sie alle Variablen der beiden Dateien CONSTANT.js von Teil 1 und Teil 2 des Tutoriums in die Datei CONSTANT.js ds dritten Teils ein.

Achtung: Definieren Sie keine Variable doppelt.

Sie werde später noch weitere Konstanten in diese Datei einfügen, wie z.B. Konstanten, die die Startposition von Ball und Schläger definieren, oder eine Konstate PADDLE_FRICTION, die die Reibung des Schlägers festlegt. (Beachten Sie, dass in den Use Cases Folgendes gefordert wurde: Wird der Schläger im Moment der Kollision bewegt, so wird der Ball abhängig von der Bewegungsrichtung und Geschwindigkeit des Schlägers abgelenkt.)

main.js

Fügen Sie die Variablen g_ball, g_paddle, g_context, g_interval der beiden Dateien main.js von Teil 1 und Teil 2 des Tutoriums in die Datei main.js des dritten Teils ein. Definieren Sie zusätzlich eine globale Variable g_score, in der der aktuelle Punktestand des Spiels abgelegt wird.

Achtung: Definieren Sie keine Variable doppelt.

Das Ball-Objekt g_ball

Erweitern Sie das Ball-Objekt g_ball um eine Initialisierungsmethode, deren Aufgabe es ist, den Ball vor Spielbeginn zu positionieren und eine Startgeschwindigkeit festzulegen:

init: function()
      { this.x  = BALL_X;
        this.y  = BALL_Y;
        this.vx =   (Math.random()<0.5?1:-1)
                  * (BALL_VX_MIN+Math.random()*(BALL_VX_MAX-BALL_VX_MIN));
        this.vy =   (BALL_VY_MIN+Math.random()*(BALL_VY_MAX-BALL_VY_MIN));
      },

Setzen Sie BALL_Y in der Datei CONSTANT.js so, dass der Ball am oberen Rand des Spielfelds zum liegen kommt.

Legen Sie im Ball-Objekt für alle Attribute sinnvolle Initialwerte fest. Diejenigen Werte, die später von der init-Methode festgelegt werden, können einfach mit Null vorbelegt werden.

Das Schläger-Objekt g_paddle

Erweitern Sie das Schläger-Objekt g_paddle um eine Initialisierungsmethode, deren Aufgabe es ist, den Schläger am unteren Bühnenrand zu positionieren und die Startgeschwindigkeit des Schlägers (in x-Richtung) auf 0 zu setzen:

init: function()
      { this.x  = PADDLE_X;
        this.y  = PADDLE_Y;
        this.vx = 0;
      },

Beachten Sie, dass der Schläger PADDLE_PADDING Pixel vom unteren Rand entfernt positioniert wird. Den Wert dieser Konstanten müssen Sie in CONSTANT.js festlegen.

Legen Sie im Schläger-Objekt für alle Attribute sinnvolle Initialwerte fest. Diejenigen Werte, die später von der init-Methode festgelegt werden, können einfach mit Null vorbelegt werden.

Definieren Sie für das Schläger-Objekt ein weiteres Attribut friction und initialisieren Sie dieses mit der Konstanten PADDLE_FRICTION.

Die Initfunktion

Die Funktion f_init unterscheidet sich nicht wesentlich von der Init-Funktion der beiden ersten Teile des Tutoriums:

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

// 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");

  // Reset the game (i.e. change its status to "waiting for game to be started")
  f_reset();
}

Neu ist lediglich der Aufruf der Funktion f_reset und das Fehlen der Timerdefinition. Der Timer wird im Spiel erst gestartet, wenn der Benutzer den Start-Knopf gedrückt hat. Die Reset-Funktion hat sowohl die Aufgabe, Ball und Schläger auf ihre jeweilige Startposition auf der Bühne zu setzen und dann die Bühne neu zu zeichnen, als auch den Start/Stopp-Button als Start-Button aktivieren. Die Reset-Funktion wird – im Gegensatz zur Init-Funktion – bei jedem Neustart des Spiels aufgerufen.

// Resets the game.
function f_reset() 
{ //Set the button "d_start_stop" to be a start button (view and behaviour).
  document.getElementById("d_start_stop").value       = "Start";
  document.getElementById("d_start_stop").onmousedown = o_start_game;

  // Puts the ball and the paddle on their starting positions.
  g_ball.init();
  g_paddle.init();
  
  // Redraw the canvas.
  f_draw();
}

Die Draw-Funktion f_draw hat nur eine Aufgabe: Neuzeichnen der Bühne, d.h. Löschen des Bühneninhalts und Neuzeichnen aller Sprites auf der Bühne. Dazu ruft sie der Reihe nach die draw-Methoden der einezelnen Sprites auf und übergibt diesen die Bühne auf der der jeweilige Sprite gezeichnet werden soll. (In unseren Fall gibt es nur zwei Sprites: Ball und Schläger.)

// Clears the canvas and redraws all sprites (ball and paddle).
function f_draw()
{ g_context.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);

  g_ball.draw(g_context);
  g_paddle.draw(g_context);
}

Zusätzlich sollte man noch eine Funktion definieren, mit der man Informationen in das Textfeld der HTML-Seite schreiben kann:

// Displays information on the HTML page.
function f_info(p_info)
{ document.getElementById("d_info").firstChild.nodeValue = p_info; }

Observer

Observer sind speziele Funktionen, die in Falle von betimmten Ereignissen (events), wie Timer-Event, Tasten-Event, Maus-Event etc. ausgeführt werden.

Die beiden Observer o_start_paddle_moving und o_stop_paddle_moving können direkt aus der Datei main.js von Teil zwei des Tutoriums übernommen werden. Sie haben die Aufgabe, den Schläger in eine bestimmte Richtung zu bewegen, solange der Spieler eine bestimmte Taste drückt.

Der Observer o_redraw wächst im Teil 3 des Tutoriums zum „Moloch“ an. Er hat die Aufgabe, eine Kollision zwischen Ball und Wand oder Ball und Schläger zu erkennen und entsprechend zu reagieren:

  • Ball und Wand (links, rechts, oben): Ball prallt ab
  • Ball und Wand (unten außerhalb des sichbaren Bereichs): Spielende
  • Ball und Schläger: Ball prallt ab und wird evtl. abgelenkt; Erhöhung des Scores um einen Punkt

Außerdem muss diese Funktion auch noch den Canvas löschen und die neu positionierten Sprites zeichnen, sofern die Funktion nicht aufgrund des Spielendes vorzeitig verlassen wurde.

//  An event observer: 
//  It is called every 1000/FPS seconds, but only when the game has been started.
function o_redraw() 
{ // Collision detection: ball <-> wall (left, right, top) 
  // 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.vy = -g_ball.vy;

  // Collision detection: ball <-> wall (bottom outside of the canvas) 
  // Collision response:  stop the game (as the ball has vanished)
  if (g_ball.y - 2*g_ball.r > CANVAS_HEIGHT)
  { o_stop_game();
    return; // leave o_redraw early
  }
 
  // Collision detection: ball <-> paddle
  // Collision response:  change the moving direction of the ball 
  //                    + change the velocity of the ball 
  //                      (due to the friction between the ball and the paddle)
  //                    + display the new score
  // Collission detection and handling has to be improved:
  // A collission with the small side of the paddle should be handled.
  if (   g_ball.y + 0.5*g_ball.r >= g_paddle.y 
      && g_ball.y + 0.5*g_ball.r <= g_paddle.y + g_paddle.height
      && g_ball.x + 0.5*g_ball.r >= g_paddle.x
      && g_ball.x - 0.5*g_ball.r <= g_paddle.x + g_paddle.width
     )
  { // Resolve penetration.
    if (g_ball.y + 0.5*g_ball.r <= g_paddle.y + g_paddle.height)
      g_ball.y = g_paddle.y - 0.5*g_ball.r;
    else
      g_ball.y = g_paddle.y + g_paddle.height + 0.5*g_ball.r;

    g_ball.vy = -g_ball.vy;
    g_ball.vx += g_paddle.friction*g_paddle.vx;
    
    // Raise thr score and display the new score
    g_score++;
    f_info("Punkte: " + g_score);
  }
  
  // Move the ball.
  g_ball.move();
  
  // Move the paddle if possible.
  if (   (g_paddle.vx < 0 && g_paddle.x > 0)
      || (g_paddle.vx > 0 && g_paddle.x + g_paddle.width < CANVAS_WIDTH)
     )
    g_paddle.move();

  // Put all sprites on the canvas.
  f_draw();

Im Spiel gibt es noch zwei weitere Observer. Der Observer o_start_game wird bei einem Klick auf den Start/Stopp-Button aufgerufen, falls diese als Start-Button fungiert. Seine Aufgabe ist es, den Score auf Null zurückzusetzen, den Start/Stopp-Button als Stopp-Button zu aktivieren, die Steuerung des Schlägers zu aktivieren, den Timer der „physics engine“ zu starten und zu guter Letzt den eine Information über den Spielstart in das Textfeld auf der Bühne zu schreiben. Aktuell könnte im letzten Schritt auch gleich der aktuelle Punktestand (nnull Punkte) angezeigt werden.

// An event observer: 
// It is called whenever a "start game event" is signalled.
function o_start_game() 
{ // Reset the score.
  g_score = 0;

  // Set the button "d_start_stop" to be a stop button (view and behaviour).
  document.getElementById("d_start_stop").value       = "Stopp";
  document.getElementById("d_start_stop").onmousedown = o_stop_game;

  // React to key down and key up events.
  document.onkeydown = o_start_paddle_moving;
  document.onkeyup   = o_stop_paddle_moving;

  // Start the timer for redrawing the canvas every 1000/FPS seconds.
  g_interval = window.setInterval("o_redraw()", 1000/FPS);

  // Inform the user about the game status (game has stared).
  f_info("Spiel gestartet");
}

Die Aufgaben des Stopp-Spiel-Observer sind etwas weniger umfangreich, da die mesiten Aufgaben bereits von der Resetfunktion wahrgenommen werden. Er muss den Physics-Engine-Timer stoppen, die Steuerung des Schlägers deaktivieren und schließlich die Resetfunktion aufrufen. Diese sorgt unter Anderem dafür, dass der Start/Stopp-Button wieder als Start-Button fungiert.

// An event observer: 
// It is called whenever a "stop game event" is signalled.
function o_stop_game()
{ // Stop the canvas redrawing timer.
  window.clearInterval(g_interval);

  // Stop reacting to key down and key up events.
  document.onkeydown = null;
  document.onkeyup   = null;

  // Reset the game.
  f_reset();
}

Anwendung testen und sichern

Testen Sie Ihre Anwendung in gewohnter Weise.

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

Probleme der Kollisionserkennung

Die in Teil 1 des Tutoriums angesprochenen Probleme de „Tunnelns“ und der „Überlappung“ bei einer A-posteriori-Kollissionserkennung kann man in dieser Minipong-Version besonders schön veranschaulichen.

Tunneln

Beispiel: modifizierte Minipong-Anwendung

In diesem Beispiel wurden einfach die Framrate deutlich reduziert (auf 10 Frames pro Sekunde) sowie eine hohe Ballgeschwindigkeit (200 Pixel pro Sekunde in y-Richtung) eingestellt.

(Damit Sie den Effekt auf jeden Fall beobachten können, wurde außerdem die Steuerung des Schlägers deaktiviert.)

Überlappung

Beispiele: modifizierte Minipong-Anwendung, modifizierte Pong-Anwendung,

Um diesen Effekt zu erreichen wurde nur eine wesentliche Änderung am Source-Code vorgenommen. In der Funktion o_redraw wurden folgende vier Zeilen auskommentiert:

if (g_ball.y + 0.5*g_ball.r <= g_paddle.y + g_paddle.height)
  g_ball.y = g_paddle.y - 0.5*g_ball.r;
else
  g_ball.y = g_paddle.y + g_paddle.height + 0.5*g_ball.r;

Diese Anweisungen verschieben den Ball im Falle einer Kollision soweit, dass er den Schläger nicht überlappt, sondern nur berührt. Wenn diese Korrekturmaßnahme unterbleibt, kann der Ball in gewissen Situationen im Schläger hängen beleiben. Eine derartige Situation kann man beispielsweise erzwingen, wenn man in der Datei CONSTANT.js der Minipong-Anwendung folgende Werte wählt:

var BALL_VX_MIN  = 115; // pixels per second
var BALL_VX_MAX  = 115; // pixels per second
var BALL_VY_MIN  =  21; // pixels per second
var BALL_VY_MAX  =  21; // pixels per second

Aber auch ohne Modifikation der Startwerte des Balls kann dieser Effekt auftreten, wenn man den Ball während des Spiels geeignet mit dem Schläger ablenkt.

Erweiterung

Erweitern Sie die Anwendung 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.

Am besten zeigen Sie die erzielten Punkte auch auf der Bühne selbst an (vgl. Bilder im [[Wikipedia:Pong Wikipedia-Artikel zu Pong]). Wie man Text auf der Bühne darstellen kann, wurde im Hello-World-Tutorium gezeigt.

Beachten Sie wieder das Programmierprinzip „Don't repeat yourself“ (Dry).

Musterlösung: Minipong 3a (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, Musterlösungen (SVN)