MMProg: Praktikum: WiSe 2018/19: Pong01

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: 4
(unwichtige Fakten fehlen)
Quellenangaben: 3
(wichtige Quellen vorhanden)
Quellenarten: 5
(ausgezeichnet)
Konformität: 3
(gut)

Vorlesung MMProg

Inhalt | EcmaScript01 | EcmaScript02 | EcmaScript03 | Ball 01| Ball 02 | Ball 03 | Pong 01

Musterlösung: Web-Auftritt (Git-Repository)

Ziel

Ziel dieser Praktikumsaufgabe ist es, das Spiel Pong zu implementieren.

Vorbereitung

Importieren Sie das leere Git-Projekt Pong01 in WebStorm. Laden Sie anschließend mittels npm i alle benötigten Node.js-Module in das Projekt.

Sie können Ihr Projekt zur Übung auch in Ihrem Git-Repository speichern. Das ist aber nicht so wichtig. Falls Sie das machen möchten, müssen Sie es zuvor von meinem (schreibgeschützten) Repository lösen:

git remote remove origin
git remote add origin https://gitlab.multimedia.hs-augsburg.de:8888/BENUTZER/Pong01.git

In Ihrem Projekt finden Sie eine Web-Anwendung: src/index00.html

Diese entspricht im Wesentlichen der Musterlösung der Aufgabe 3 des Tutoriums Ball03. Allerdings wurde für die Spielbühne eine feste Größe gewählt, das Bild des Balls sowie das CSS wurden verändert und ein Splashscreen wurde eingeführt. Dieser wird mittels CSS-Transitionen und JavaScript-await-Befehlen in der Initfunktion der Datei app00/app.js implementiert. Die await-Befehle sowie die zugehörigen Splaschscreen-Befehle ändern sich im Laufe des Tutoriums nicht. Sie sollten sie aber trotzdem studieren, wenn Sie daran interessiert sind, wie sie funktionieren.

Beachten Sie, dass die Klasse ModelCircle vier Methoden left, right, top und bottom enthält. Mit diesen Methoden werden die Ränder des Kreises ermittelt. Damit kann man Kollisionsberechnungen etwas einfacher formulieren.

Bei den Methoden left, right, top und bottom handelt es sich nicht um normale Methoden, sondern um so genannte Getter-Methoden (MDN web docs: Getter). Getter-Methoden sind Methoden ohne Parameter, vor die das Schlusselwort get geschrieben wird. Sie werden zur Berechnung von Attributwerten verwendet werden.

get left() { return this.x - this.r; }
// Der linke Rand eines Kreise ist gleich 
// seiner Position x abzüglich seines Radius r.

Eine normale Methode würde man mittels console.log(myCircle.left()); aufrufen. Beim Zugriff auf eine Getter-Methode verwendet man dagegen die klammerfreie Attributzugriff-Syntax:

console.log(myCircle.left);

Neben den Getter-Methoden gibt es auch noch Setter-Methoden, die dazu dienen, berechnete Attribute zu verändern (MDN web docs: Setter). Diese haben genau einen Parameter, der den Wert enthält, der gespeichert werden soll:

set left(p_x) { this.x = p_x + this.r; }
// Anstelle des linken Randes wird die Position des Kreises geändert,
// und zwar so, dass der linke Rand an der gewünschten Position zu
// liegen kommt.

Zum Aufruf der Setter-Methoden kommt ebenfalls die Attributzugriff-Syntax zum Einsatz:

myCircle.left = 100;
console.log(myCircle.left)  100

Use Cases

Use Cases des Spiels Pong

Der Aufgabe liegt das Modell Pong/Modellierung zugrunde. Allerdings wurden bei der Umsetzung einige Änderungen vorgenommen. Am Use-Case-Diagramm fällt auf, dass der Use Case „Spiel abbrechen“ fehlt. Es wurde darauf verzichtet, da nicht – wie ursprünglich geplant – ein Start-/Stopp-Knopf (als HTML-Button) außerhalb der Bühne platziert wird, sondern nur ein Start-Knopf innerhalb der Bühne. Dieser wird ausgeblendet, solange das Spiel läuft.

Ich habe das implementierte Modell ganz bewusst gegenüber der ursprünglichen Planung abgeändert, um den dynamischen Prozess zu verdeutlichen, den ein Modell durchläuft. Es ändert sich ständig: Elemente werden ergänzt, verfeinert, ersetzt oder auch ersazlos gestrichen. Ich kenne niemanden, der zu Beginn eines Projektes ein perfektes Modell aufstellt, dass nicht ein paar Dutzend mal geändert werden muss.

Klassendiagramm (Moduldiagramm)

[Klassendiagramm von Pong01 (zweite Version)
 
WK Pong01 ClassModel01 2018 02.png
[Klassendiagramm von Pong01 (erste Version)
 
WK Pong01 ClassModel01 2018.png

Dieses Klassendiagramm ist ebenfalls gegenüber dem Klassendiagramm von Pong/Modellierung weiterentwickelt worden. Anstelle eines HTML-Elements gibt es jetzt einen kreisförmigen Start-Button. Die Klassen ModelCircle und ViewCircle werden auch für den Ball verwendet (Wiederverwendung). Die Klasse ModelPaddle wurde als Unterklasse einer (wiederverwendbaren) Klasse ModelRectangle definiert. Für die Punkteanzeige wurde eine eigene Klasse ModelScore definiert, da in Objekten der Klasse ModelText keine Zahlen, sondern nur Text gespeichert werden können. Das ist für die Rechnung mit Punkten, die jeweils mittels ++ erhöht werden, etwas unpraktisch. Zu guter Letzt wurde noch ein Textfeld eingeführt, das dazu genutzt werden kann, bei Spielende die Spieler über den Sieger zu informieren.

Aufgabe

Implementieren Sie Pong gemäß obigem Klassendiagramm.

Aufgabe 1

(Musterlösung: Gitlab: WK_Pong01, app01, index01.html)

Sie sollten zunächst eine Kopie Ihrer Web-App erstellen:

  • Erstellen Sie eine Kopie des Ordners src/js/app00 unter dem Namen src/js/app01.
  • Erstellen Sie eine Kopie der Datei src/index00.html unter dem Namen src/index01.html und ändern Sie den Titel in dieser Datei entsprechend.
  • Starten Sie gegebenenfalls npm run watch neu (Abbruch des Watchers: Strg-c bzw. Crtl-c).

In der Musterlösung app00 aus Ball 03 gibt es schon einige Module, die Sie wiederverwenden können:

  • model/ModelStage
  • model/ModelCircle
  • model/collisionCircleStage
  • model/update
  • view/ViewCircleGraphics
  • view/ViewCircleSprite
  • view/render
  • app

Implementieren Sie als nächstes folgende Module, um die beiden Schläger darstellen zu könnnen:

  • model/ModelRectangle (ModelPaddle folgt später)
  • view/ViewRectangleGraphics

Klasse ModelRectangle

Die Klasse ModelRectangle hat folgende Attribute:

  • width (anstelle von r in ModelCircle)
  • height (anstelle von r in ModelCircle)
  • x (wie ModelCircle)
  • y (wie ModelCircle)
  • vx (wie ModelCircle)
  • vy (wie ModelCircle)
  • ax (vgl. Praktikum Ball01, Aufgabe 6)
  • ay (vgl. Praktikum Ball01, Aufgabe 6)
  • left (analog zu ModelCircle ohne this.r)
  • right (analog zu ModelCircle, aber mit this.width)
  • top (analog zu ModelCircle ohne this.r)
  • bottom (analog zu ModelCircle, aber mit this.width)

Darüber hinaus enthält diese Klasse zwei Methoden:

  • reset (analog zu ModelCircle)
  • update (analog zu ModelCircle, allerdings muss auch die Geschwindigkeit abhängig von der Beschleunigung angepasst werden; vgl. Praktikum Ball01, Aufgabe 6)

Beachten Sie bei der Berechnung der der Begrenzungen left, right, top und bottom eines Rechtecks, dass der Ankerpunkt üblicherweise in der linken oberen Ecke des Rechtecks liegt. Beim Kreis liegt er dagegen im Mittelpunkt.

Klasse ViewRectangleGraphics

Die Klasse ViewRectangleGraphics wird analog zu ViewCircleGraphics implementiert. Der wesentliche Unterschied ist, dass das grafische Objekt nicht mit drawCircle, sondern mittels drawRect gezeichnet wird (PIXI.Graphics).

config.json und Modul app.js

Jetzt können Sie zwei Schläger in Ihre Anwendung einbinden. Erweitern Sie zunächst die Kondifurationsdatei config/config.json. Fügen Sie in das Modelobjekt die Modelkonfigurationen der beiden Paddle ein:

"paddles":
[ { "x":        5,
    "y":      170,
    "vy":     150,
    "ay":    1000,
    "width":   10,
    "height":  60
  },
      
  { "x":      585,
    "y":      170,
    "vy":     150,
    "ay":    1000,
    "width":   10,
    "height":  60
   }
]

Und in das Viewobjekt fügen Sie eine View-Konfiguration ein, die für beide Paddle verwendet werden kann.

"paddle":
{ "border": 0,
  "color":  { "color": "#999999" }
}

Wenn Sie keinen Syntaxfehler gemacht haben, lässt sich die app01 immer noch fehlerfrei übersetzen.

Nun ist es an der Zeit die Datei app.js anzupassen:

  • Importieren Sie die beiden neu erstellten Klassen ModelRectangle und ViewRectangleGraphics analog zu ModelCircle und ViewCircleGraphics. Auch hier gilt: Wenn Sie in beiden Dateien keine Syntaxfehler gemacht haben, lässt sich die App immer noch fehlerfrei übersetzen.
  • Fügen Sie nach der Konstanten c_config_view_ball zwei Konstanten ein, mit denen Sie auf die beiden Konfigurationsobjekte zugreifen können, die Sie zuvor in die Datei config.json eingefügt haben:
c_config_model_paddles = c_config_model.paddles,
c_config_view_paddle   = c_config_view.paddle,
  • Nun müssen Sie im Anschluss an die Erzeugung von c_model_ball die Models der beiden Schläger erzeugen (wären es mehr als zwei Objekte, würde man das Array natürlich mit Hilfe einer Schleife füllen):
c_model_paddles =
  [ new ModelRectangle(c_config_model_paddles[0]),
    new ModelRectangle(c_config_model_paddles[1])
  ]
  • Als nächstes müssen Sie das soeben erzeugte Array in das Konfigurationsobjekt vom Funktionsaufruf der Funktion initUpdater einfügen, da der Updater natürlich auch die Positionen der Schläger regelmäßig neu berechnen muss:
initUpdater({stage:   c_model_stage,
             ball:    c_model_ball,
             paddles: c_model_paddles
           });
  • Jetzt fehlen noch die Views. Diese müssen im Rumpf der Initfunktion im Anschluss an die Ballview erzeugt werden (auch hier gilt: das Array würde mit Hilfe eine Schleife befüllt werden, wenn es sich um mehr als zwei Schläger handeln würde):
c_views_paddle =
[ new ViewRectangleGraphics
      (c_pixi_app, c_model_paddles[0],c_config_view_paddle),
  new ViewRectangleGraphics
      (c_pixi_app, c_model_paddles[1], c_config_view_paddle)
]
  • Diese Views müssen vom Renderer regelmäßig neu gezeichnet werden (hier kommt wieder ES-6-Destructuring-Syntax zum Einsatz, um den Inhalt des Arraya c_views_paddle in ein anderes Array einzugügen; in ES 5 müssten Sie initRenderer([c_view_ball].concat(c_views_paddle)); schreiben)
initRenderer([c_view_ball, ...c_views_paddle]);
  • Jetzt sollte die Anwendung wieder laufen und die beiden Schläger sollten zu sehen sein.

Modul update

Sie sollten auch noch das Update-Modul update aktualisieren. Die Schläger bewegen sie noch nicht, aber sie sollten sich bewegen, da in der Konfigurationsdatei für beide Schläger eine Geschwindigkeit und eine Beschleunigung in y-Richtung eingetragen wurde. Das heißt, das Update-Modul sollte dafür sorgen, dass für beide Schläger regelmäßig die Updatefunktion aufgerufen wird.

  • Fügen Sie den den Parameter paddles: p_paddles in das Config-Objekt der Parameterliste der Funktion initUpdater ein.
  • Speichern Sie das im Parameter p_paddles übergebene Array mit den ModelRectangle-Objekten in einer globalen (aber privaten) Variablen v_paddles des Moduls.
  • Fügen Sie in die Methode update eine For-Loop ein, die für jedes Objekt im Array v_paddles die zugehörige Update-Methode (mit einem geeigneten Argument) aufruft.
  • Testen Sie, ob sich das Programm noch korrekt übersetzen lässt. Wenn Sie es laufen lassen, sollten die beiden Schläger fluchtartig die Bühne verlassen:
    https://glossar.hs-augsburg.de/beispiel/tutorium/2018/pong/WK_Pong01/web/index01.html

Aufgabe 2

(Musterlösung: Gitlab: WK_Pong01, app02, index02.html)

Sorgen Sie jetzt dafür, dass die Kollisionen der Paddle mit dem Ball und der Bühne erkannt und behandelt werden.

  • Fügen Sie in die Klassen ModelCircle und ModelRectangle Setter-Methoden für die Attribute left, right, top und bottom ein. Diese sollen dafür sorgen, dass jeweils die $x$- bzw. die $y$-Koordinate des Objektes so gesetzt wird, dass die Getter-Methode des modifizierten Randattriutes den geünschten Wert als Ergebnis liefert.
  • Definieren Sie die Klasse ModelPaddle als Subklasse der Klasse ModelRectangle. Der Konstruktor constructor(p_config) übergibt das Konfigurationsobject p_config einfach an seine Superklasse: super(p_config);. Überschreiben Sie anschließend die Resetfunktion. Diese soll die Breite, Höhe und Position des Schlägers genauso wie die KlasseModelRectangle auf die im Konfigurationsobjekt übergebenen Werte zurücksetzen. Die Geschwindigkeit und die Beschluenigung soll sie allerdings jeweils auf 0 setzen, da die Schläger sich zu Beginn nicht bewegen.
  • Verwenden Sie im Modul app die Klasse ModelPaddle anstelle der Klasse ModelRectangle zum Erzeugen der beiden Schläger-Models.
  • Fügen Sie in die Klasse ModelPaddle drei Methoden down, up und stop ein.
    • Die Methode down setzt die Geschwindigkeit und die Beschleunigung in y-Richtung auf die im Konfigurationsobjekt übergebenen Werte, sofern die Geschwindigkeit beim Aufruf gleich 0 ist.
    • Die Methode up setzt die Geschwindigkeit und die Beschleunigung in y-Richtung auf die im Konfigurationsobjekt übergebenen Werte, allerdings mit negativem Vorzeichen, sofern die Geschwindigkeit beim Aufruf gleich 0 ist.
    • Die Methode stop setzt die Geschwindigkeit und die Beschleunigung in y-Richtung auf 0, sofern die Geschwindigkeit beim Aufruf ungleich 0 ist.
    • Definieren Sie eine Modul collisionPaddleStage(p_paddle, p_stage), das das übergebene Paddle mittels der zuvor definierten Methode stop anhält und vollständig zurück auf die Bühne verschiebt, sobald es mit einem Bühnenrand kollidiert. Sie können hierzu die zuvor definierten Setter-Methoden verwenden: p_paddle.top = p_stage.top; bzw. p_paddle.bottom = p_stage.bottom;.
    • Wenn Sie möchten, können Sie auch noch gleich die Kollisionserkennung und -behandlung von Kreis und Bühne (collisionCircleStage) vereinfachen, indem Sie die zuvor definierten Attribute left, right ... verwenden.
    • Schreiben Sie eine Kollisionererkennung- und behandlung collisionPaddleCircle(p_paddle, p_circle) für Schläger und Ball. Folgender Code funktioniert, aber nur schlecht, falls der Ball mit einer Ecke eines Schläger kollidiert. Hier bestehnt noch erhebliher Verbesserungsbedarf:
function collisionPaddleCircle(p_paddle, p_circle)
{ if (p_circle.y + 0.8*p_circle.r >= p_paddle.top &&
      p_circle.y - 0.8*p_circle.r <= p_paddle.bottom
     )
  { if (p_circle.vx > 0 &&  // The ball is moving from left to right.
        p_circle.right >= p_paddle.left && p_circle.left < p_paddle.right
       )
    { p_circle.right = p_paddle.left;
      p_circle.vx    = -p_circle.vx;
    }

    if (p_circle.vx < 0 &&  // The ball is moving from right to left.
        p_circle.left <= p_paddle.right && p_circle.right > p_paddle.left
       )
    { p_circle.left = p_paddle.right;
      p_circle.vx   = -p_circle.vx;
    }
  }
}
  • Importieren Sie die beiden Funktionen collisionPaddleStage und collisionPaddleCircle und das Update-Modul und runfen Sie die beiden Funktionen innerhalb dr Schleife der Update-Funktion geeignet auf.

Wenn sich alles übersetzen lässt, sollte die App wieder laufen. Die Schläger sollten sich nicht bewegen, aber der Ball sollte davon abprallen, sobald er mit einem kollidiert. Sie können testhalber als letzen Befehl (d. h. nach dem Start der Game Loop) folgenden Befehl in den Rumpf der Init-Funktion im Modul app einfügen:

c_model_paddles[1].up();

Damit sollte sich – sofern Sie Ihr Array mit den beiden Schläger-Models c_model_paddles genannt haben – der rechte Schläger nach oben bewengen, sobald das Spiel gestartet wurde. Und er sollte stehen bleiben, sobald er mit dem oberen Rand kollidiert:
https://glossar.hs-augsburg.de/beispiel/tutorium/2018/pong/WK_Pong01/web/index02.html

Sie sollten testweise auch den anderen Schläger nach oben oder unten bewegen.

Aufgabe 3

(Musterlösung: Gitlab: WK_Pong01, app03, index03.html)

Als nächstes sollten Sie eine Contoller für die Schäger einbauen:

  • Fügen Sie in die Konfigurationsdatei nach "model" und "view" ein drittes Objekt ein, das beschreibt, mit welchen Tasten die beiden Schläger gesteuert werden sollen:
"control":
{ "paddles":
  [ { "up":   "w",
      "down": "x"
    },
    { "up":   "ArrowUp",
      "down": "ArrowDown"
    }
  ]
}
  • Legen Sie eine neue Datei control/ControlPaddle.js an, die einen Klasse ControlPaddle definiert und eportiert. Diese Klasse enthält lediglich einen Kostruktor. Dieser definiert zwei interne Funktionen o_start_moving und o_stop_moving (o_ steht für Observer Pattern '''O'''bserver), die als Event Handler für Tastaturereignisse verwendet werden (vgl. [Hello-World-Tutorium, Teil 3, Aufgabe 6]). Wenn eine der beiden Tasten im Konfigurationsobjekt gedrückt werden (window-keydown-Ereignis), wird der Schläger mittels einer der zuvor definierten Methoden up oder down n die gewünschte Richtung bewegt. Sobald die Taste wieder losgelassen wird (window-keyup-Ereignis) wird der Schläger mittels der Methode stop wieder angehalten.
constructor(p_paddle,
            { up: p_up = 'ArrowUp', down: p_down = 'ArrowDown'} = {}
           )
{ function o_start_moving(p_event)
  { if (p_event.key === p_up)
    { p_paddle.up(); }
    else if (p_event.key === p_down)
    { p_paddle.down(); }
  }
  
  function o_stop_moving(p_event)
  { if (p_event.key === p_up || p_event.key === p_down)
    { p_paddle.stop(); }
  }
  
  window.addEventListener("keydown", o_start_moving);
  window.addEventListener("keyup",   o_stop_moving);
}
  • Nun müssen Sie noch diController für die beiden Schläger im Modul app erstellen und initialisieren:
    • Importieren Sie zunächst die neu definierte Klasse ControlPaddle.
    • Definieren Sie dann analog zu den anderen Konstanten, in denen Sie bestimmte Teilobjekte der Konfigurationsdatei speichern, zwei Konstanten c_config_control = config.control sowie c_config_control_paddles = c_config_control.paddles. In der zweiten Konstante wird das Konfigurationsobjekt für den Paddle-Controller gespeichert, das Sie zuvor in die Datei config.json eingefügt haben.
    • Erzeugen und initialisieren Sie nun im Anschluss an die Konstantendefinitionen die beiden Controller. Beschten Sie, dass es nicht notwendig ist, die beiden Objekte zu speichern, da kein Modul wieder darauf zugreifen wird.
new ControlPaddle(c_model_paddles[0], c_config_control_paddles[0]);
new ControlPaddle(c_model_paddles[1], c_config_control_paddles[1]);

Wenn Sie jetzt die Anwendung starten, sollten Sie die beiden Schläger mittels der Tasten w und x bzw. ArrowUp und ArrowDown bewegen können. Der Ball sollte abprallen, wenn er mit einem kollidiert:
https://glossar.hs-augsburg.de/beispiel/tutorium/2018/pong/WK_Pong01/web/index03.html

Aufgabe 4

(Musterlösung: Gitlab: WK_Pong01, app04, index04.html)

Nun ist es an der Zeit, die Spiellogik zu implementieren.

  • Fügen Sie ein paar neue Informationen in die Konfigurationsdatei ein:
    • "model": Hier werden Informationen über die beiden Score-Textfelder erfasst: Position und Startwert:
"scores":
[ { "x":     20,
    "y":     20,
    "score":  0
  },  
  { "x":    560,
    "y":     20,
    "score":  0
  }
]
    • "model": Hier wird festgelegt, wie eine (Score-)Textfeld formartiert ("style") und positioniert (anchor; 0.5 entspricht zentriert) werden soll:
"text":
{ "style": { "fontFamily": ["Verdana", "Helvetica", "sans-serif"],
             "fontSize":   "20px"
           },
  "anchor": {"x": 0.5, "y": 0}
}
    • "logic": Ein neuer Eintrag an Ender der Konfigurationsdatei, in der festgeleft wird, wie viele Runden das Spiel dauern soll:
{ "winScore": 3 }
  • Ersetzen Sie in der Konfigurationsdatei nun noch im Ballobjekt die festen Geschwidigkeitswerte "vx": 200, "vy": 150 durch die nachfolgenden Objekte. Diese können später in der Resetfunktion von ModelCircle mittels concretize durch Zufallswerte in den angegebenen Bereichen ersetzt werden. Ermittelt wird jeweils einen Zufallszahl im Intervalll 150 bis 300 und wird mit 50-prozentiger Wahrscheinlichkeit mit einen negativen Vorzeichen versehen (vgl. Praktikumsaufgabe Ball03, Aufgabe 2):
"vx": {"@min": 150, "@max": 300, "@positive": 0.5},
"vy": {"@min": 150, "@max": 300, "@positive": 0.5}

Wenn alles funktioniert, sollten Sie ein erstes Spiel wagen können.
https://glossar.hs-augsburg.de/beispiel/tutorium/2018/pong/WK_Pong01/web/index04.html

Aufgabe 5

(Musterlösung: Gitlab: WK_Pong01, app05, index05.html)

Aufgabe 6

(Musterlösung: Gitlab: WK_Pong01, app06, index06.html)

Quellen

  1. Kowarschick (MMProg): Wolfgang Kowarschick; Vorlesung „Multimedia-Programmierung“; Hochschule: Hochschule Augsburg; Adresse: Augsburg; Web-Link; 2018; Quellengüte: 3 (Vorlesung)