MMProg: Praktikum: WiSe 2018/19: Pong01: Unterschied zwischen den Versionen

aus GlossarWiki, der Glossar-Datenbank der Fachhochschule Augsburg
Keine Bearbeitungszusammenfassung
 
(52 dazwischenliegende Versionen desselben Benutzers werden nicht angezeigt)
Zeile 1: Zeile 1:
{MMProg:Praktikum:WiSe 2018/19:Menü}}
{{MMProg:Praktikum:WiSe 2018/19:Menü}}


'''Musterlösung''': [https://glossar.hs-augsburg.de/beispiel/tutorium/2018/ball/WK_Pong01/web/ Web-Auftritt] ([https://gitlab.multimedia.hs-augsburg.de:8888/kowa/WK_Pong01.git Git-Repository])  
'''Musterlösung''': [https://glossar.hs-augsburg.de/beispiel/tutorium/2018/pong/WK_Pong01/web/ Web-Auftritt] ([https://gitlab.multimedia.hs-augsburg.de/kowa/WK_Pong01.git Git-Repository])  


==Ziel==
==Ziel==
Zeile 8: Zeile 8:


==Vorbereitung==
==Vorbereitung==
Importieren Sie das leere Git-Projekt [https://gitlab.multimedia.hs-augsburg.de:8888/kowa/Pong01.git Pong01] in WebStorm.
Importieren Sie das leere Git-Projekt [https://gitlab.multimedia.hs-augsburg.de/kowa/Pong01.git Pong01] in WebStorm.
Laden Sie anschließend mittels <code>npm i</code> alle benötigten Node.js-Module in das Projekt.
Laden Sie anschließend mittels <code>npm i</code> alle benötigten Node.js-Module in das Projekt.


Zeile 16: Zeile 16:
<source lang="javascript">
<source lang="javascript">
git remote remove origin
git remote remove origin
git remote add origin https://gitlab.multimedia.hs-augsburg.de:8888/BENUTZER/Pong01.git
git remote add origin https://gitlab.multimedia.hs-augsburg.de/BENUTZER/Pong01.git
</source>
</source>


In Ihrem Projekt finden Sie eine Web-Anwendung: <code>src/index00.html</code>. Diese entspricht im Wesentlichen der Musterlösung der  
In Ihrem Projekt finden Sie eine Web-Anwendung: <code>src/index00.html</code><br/>
* https://glossar.hs-augsburg.de/beispiel/tutorium/2018/pong/WK_Pong01/web/index00.html
 
Diese entspricht im Wesentlichen der Musterlösung der  
Aufgabe 3 des Tutoriums [[MMProg: Praktikum: WiSe 2018/19: Ball03|Ball03]]. Allerdings wurde für die Spielbühne eine feste Größe gewählt,
Aufgabe 3 des Tutoriums [[MMProg: Praktikum: WiSe 2018/19: Ball03|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. diese wird mittels CSS-Transitionen und
das Bild des Balls sowie das CSS wurden verändert und ein Splashscreen wurde eingeführt. Dieser wird mittels CSS-Transitionen und
JavaScript <code>await</code>-Befehlen in der Initfuktion der Datei <code>app00/app.js</code> erreicht. Die <code>await</code>-Befehle  
JavaScript-<code>await</code>-Befehlen in der Initfunktion der Datei <code>app00/app.js</code> implementiert. Die <code>await</code>-Befehle  
sowie die zugehörigen Splaschscreen-Befehle ändern sich im Laufe des Tutoriums nicht. Sie sollten sie aber trotzdem studieren,  
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.  
wenn Sie daran interessiert sind, wie sie funktionieren.  
Beachten Sie, dass die Klasse <code>ModelCircle</code> vier Methoden <code>left</code>, <code>right</code>, <code>top</code> und <code>bottom</code> enthält. Mit diesen Methoden werden
die Ränder des Kreises ermittelt. Damit kann man Kollisionsberechnungen etwas einfacher formulieren.
Bei den Methoden <code>left</code>, <code>right</code>,
<code>top</code> und <code>bottom</code> handelt es sich nicht um normale Methoden, sondern um so
genannte [[Getter-Methode]]n ([https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Functions/get MDN web docs: Getter]). Getter-Methoden sind Methoden ohne Parameter, vor die das Schlusselwort
<code>get</code> geschrieben wird. Sie werden zur Berechnung von Attributwerten verwendet werden.
<source lang="javascript">
get left() { return this.x - this.r; }
// Der linke Rand eines Kreise ist gleich
// seiner Position x abzüglich seines Radius r.
</source>
Eine normale Methode würde man mittels <code>console.log(myCircle.left());</code> aufrufen. Beim Zugriff auf eine Getter-Methode verwendet man dagegen die klammerfreie Attributzugriff-Syntax:
<source lang="javascript">
console.log(myCircle.left);
</source>
Neben den Getter-Methoden gibt es auch noch Setter-Methoden, die dazu dienen,
berechnete Attribute zu verändern ([https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Functions/set MDN web docs: Setter]). Diese haben genau einen Parameter, der den Wert enthält, der
gespeichert werden soll:
<source lang="javascript">
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.
</source>
Zum Aufruf der Setter-Methoden kommt ebenfalls die Attributzugriff-Syntax zum Einsatz:
<source lang="javascript">
myCircle.left = 100;
console.log(myCircle.left) → 100
</source>


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


Zeile 37: Zeile 77:
um den dynamischen Prozess zu verdeutlichen, den ein Modell durchläuft. Es ändert sich ständig:
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,
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.
der zu Beginn eines Projekts ein perfektes Modell aufstellt, dass nicht ein paar Dutzend mal geändert werden muss.


===Klassendiagramm (Moduldiagramm)===
===Klassendiagramm (Moduldiagramm)===
Zeile 48: Zeile 88:


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


===Aufgabe 1===
===Aufgabe 1===
(Musterlösung: [https://gitlab.multimedia.hs-augsburg.de:8888/kowa/WK_Pong01.git Gitlab: <code>WK_Pong01</code>, <code>app01</code>]
(Musterlösung: [https://gitlab.multimedia.hs-augsburg.de/kowa/WK_Pong01.git Gitlab: <code>WK_Pong01</code>, <code>app01</code>], [https://glossar.hs-augsburg.de/beispiel/tutorium/2018/pong/WK_Pong01/web/index01.html index01.html])


Sie sollten zunächst eine Kopie Ihrer Web-App erstellen:
Sie sollten zunächst eine Kopie Ihrer Web-App erstellen:
* Erstellen Sie eine Kopie des Ordners <code>src/js/app00</code> unter dem Namen <code>src/js/app01</code>.
* Erstellen Sie eine Kopie des Ordners <code>src/js/app00</code> unter dem Namen <code>src/js/app01</code>.
* Erstellen Sie eine Kopie der Datei <code>src/index00.html</code> unter dem Namen <code>src/index01.html</code> und  ändern Sie den Titel in dieser Datei entsprechend.
* Erstellen Sie eine Kopie der Datei <code>src/index00.html</code> unter dem Namen <code>src/index01.html</code> und  ändern Sie den Titel in dieser Datei entsprechend.
* Starten Sie gegebenenfalls <code>npm run watch</code> neu (Abbruch des Watschers: <code>Strg-c</code> bzw. <code>Crtl-c</code>).
* Starten Sie gegebenenfalls <code>npm run watch</code> neu (Abbruch des Watchers: <code>Strg-c</code> bzw. <code>Crtl-c</code>).


In der Musterlösung <code>app00</code> aus Ball 03 gibt es schon einige Module, die Sie wiederverwenden können:
In der Musterlösung <code>app00</code> aus Ball 03 gibt es schon einige Module, die Sie wiederverwenden können:
Zeile 81: Zeile 121:
* <code>view/ViewRectangleGraphics</code>
* <code>view/ViewRectangleGraphics</code>


====<code>ModelRectangle</code>====
====Konfiguration des Balls====
 
Platzieren Sie den Ball zu Beginn des Spiels in der Mitte der Bühne, indem Sie die Konfigurationsdatei
entsprechend abändern. Sie können auch noch die Ballgröße anpassen, indem Sie ein beispielsweise einen
Radius von nur 12 Pixeln wählen:
<source lang="javascript">
"ball":
{ "r":  15,
  "x":  15,
  "y":  15,
  "vx": 200,
  "vy": 150
}
</source>
 
====Klasse <code>ModelRectangle</code>====


Die Klasse <code>ModelRectangle</code> hat folgende Attribute:
Die Klasse <code>ModelRectangle</code> hat folgende Attribute:
Zeile 90: Zeile 145:
*<code>vx</code> (wie <code>ModelCircle</code>)
*<code>vx</code> (wie <code>ModelCircle</code>)
*<code>vy</code> (wie <code>ModelCircle</code>)
*<code>vy</code> (wie <code>ModelCircle</code>)
*<code>ax</code> (vgl. [[MMProg:_Praktikum:_WiSe_2018/19:_Ball01#Aufgabe_6|PRaktikum Ball01, Aufgabe 6]])
*<code>ax</code> (vgl. [[MMProg:_Praktikum:_WiSe_2018/19:_Ball01#Aufgabe_6|Praktikum Ball01, Aufgabe 6]])
*<code>ay</code> (vgl. [[MMProg:_Praktikum:_WiSe_2018/19:_Ball01#Aufgabe_6|PRaktikum Ball01, Aufgabe 6]])
*<code>ay</code> (vgl. [[MMProg:_Praktikum:_WiSe_2018/19:_Ball01#Aufgabe_6|Praktikum Ball01, Aufgabe 6]])
*<code>left</code> (analog zu <code>ModelCircle</code> ohne <code>this.r</code>)
*<code>left</code> (analog zu <code>ModelCircle</code> ohne <code>this.r</code>)
*<code>right</code> (analog zu <code>ModelCircle</code>, aber mit <code>this.width</code>)
*<code>right</code> (analog zu <code>ModelCircle</code>, aber mit <code>this.width</code>)
Zeile 97: Zeile 152:
*<code>bottom</code>  (analog zu <code>ModelCircle</code>, aber mit <code>this.width</code>)
*<code>bottom</code>  (analog zu <code>ModelCircle</code>, aber mit <code>this.width</code>)


Darüber hinaus enthält diese Klasse zwei Methoden
Darüber hinaus enthält diese Klasse zwei Methoden:
*  <code>reset</code> (analog zu <code>ModelCircle</code>)  
*  <code>reset</code> (analog zu <code>ModelCircle</code>)  
* <code>update</code> (analog zu <code>ModelCircle</code>, allerdings muss auch die Geschwindigkeit abhängig von der Beschleunigung angepasst werden; vgl. [[MMProg:_Praktikum:_WiSe_2018/19:_Ball01#Aufgabe_6|PRaktikum Ball01, Aufgabe 6]])  
* <code>update</code> (analog zu <code>ModelCircle</code>, allerdings muss auch die Geschwindigkeit abhängig von der Beschleunigung angepasst werden; vgl. [[MMProg:_Praktikum:_WiSe_2018/19:_Ball01#Aufgabe_6|Praktikum Ball01, Aufgabe 6]])  
 
Beachten Sie, dass für die Berechnung der Begrenzungen <code>left</code>, <code>right</code>,
<code>top</code> und <code>bottom</code> von Kreis und Rechteck so genannte
[[Getter-Methode]]n zum Einsatz kommen ([https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Functions/get MDN web docs: Getter]).
 
<source lang="javascript">
get left()  { return this.x - this.r; }
// Der linke Rand eines Kreise ist gleich seiner Position x abzüglich seines Radius r.
</source>
Bei Getter-Methoden handelt es sich um Methoden ohne Parameter. Sie
werden zur Berechnung von Attributwerten verwendet werden. Eine normale Methode würde man mittels <code>console.log(myCircle.left())</code> aufrufen. Beim Zugriff auf eine Getter-Methode verwendet man dagegen die klammerfreie Attributzugriff-Syntax:
 
<source lang="javascript">
console.log(myCircle.left)
</source>
 
Neben den Getter-Methoden gibt es auch noch Setter-Methoden, die dazu dienen,
berechnet Attribute zu verändern ([https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Functions/set MDN web docs: Setter]). Diese haben genau einen Parameter, der den Wert enthält, der
gespeichert werden soll:
 
<source lang="javascript">
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.
</source>
 
Zum Aufruf der Setter-Methoden kommt ebenfalls die Attributzugriffs-Syntax zum Einsatz:
 
<source lang="javascript">
myCircle.left = 100;
console.log(myCircle.left) → 100
</source>


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


====<code>ViewRectangleGraphics</code>====
====Klasse <code>ViewRectangleGraphics</code>====


Die Klasse <code>ViewRectangleGraphics</code> wird analog zu  
Die Klasse <code>ViewRectangleGraphics</code> wird analog zu  
<code>ViewCircleGraphics</code> implementiert. Der wesentliche Unterschied ist, dass
<code>ViewCircleGraphics</code> implementiert. Der wesentliche Unterschied ist, dass
das grafische Objekt nicht mit <code>drawCircle</code>, sondern mit <code>drawRect</code>
das grafische Objekt nicht mit <code>drawCircle</code>, sondern mittels <code>drawRect</code>
gezeichnet wird ([http://pixijs.download/dev/docs/PIXI.Graphics.html#drawRect PIXI.Graphics]).
gezeichnet wird ([http://pixijs.download/dev/docs/PIXI.Graphics.html#drawRect PIXI.Graphics]).
<!--
==Aufgabe 1==


Zur Implementierung von [[Pong]] können Sie auf Module aus dem Praktikum zurückgreifen.
====<code>config.json</code> und Modul <code>app.js</code>====
Im obigen Diagramm sind nur einige Module farbig eingezeichnet.
Diese Module müssen Sie entweder erstellen oder  – wenn sie schon existieren – an Ihre Gegebenheiten anpassen.


Die grauen Module können Sie direkt aus der <code>wk</code>-Library einbinden, die Sie unter <code>src/js/lib/wk</code> finden.
Jetzt können Sie zwei Schläger in Ihre Anwendung einbinden. Erweitern Sie zunächst
Der zugehörige Import-Befehl lautet jeweils
die Kondifurationsdatei <code>config/config.json</code>. Fügen Sie '''in''' das
<source lang ="javascript">
Modelobjekt die Modelkonfigurationen der beiden Paddle ein:
import ... from 'wk/...';
</source>
Mehrere Beispiele finden Sie in der Datei <code>game01/game.js</code>.
Um die WebStorm-Fehlermeldungen zu vermeiden, die angeben, dass ein <code>wk</code>-Modul
nicht gefunden werden kann, sollten Sie Folgendes machen:
* Rechtsklick auf <code>src/js/lib/wk</code> → <code>Mark Directory as</code> → <code>Resource Root</code>
Falls die <code>wk</code>-Modul dann immer noch als fehlerhaft markiert werden,
sollten Sie WebStorm neu starten.


'''Achtung''': In der aktuellen Version 2017.2.5 von WebStorm werden Import-Anweisungen der Art <code>import ... from 'wk/.../MODUL.js';</code>
<source lang="javascript">
als fehlerhaft angezeigt. In der ursprünglichen Version des Projektes <code>WK_Pong01_Empty</code> wurden die Module
"paddles":
der <code>wk</code>-Bibliothek noch auf diese Weise importiert. Wenn Sie die WebStorm-Fehlermeldung stört, sollten Sie in
[ { "x":       5,
diesen Import-Befehlen die Datei-Endung <code>.js</code> entfernen: <code>import ... from 'wk/.../MODUL';</code>.
    "y":      170,
(Für Webpack wäre das nicht nötig, da Webpack weiterhin beide Schreibweisen unterstützt.)
    "vy":    150,
Die Importbefehle derjenigen Dateien, deren Importpfad mit <code>./</code> oder <code>../</code> dürfen Sie dagegen nicht ändern.
    "ay":    1000,
Diese '''müssen''' auf <code>.js</code> enden, da anderenfalls die zugehörigen Module im Verzeichnis <code>node_modules</code> gesucht werden,
    "width":  10,
wo sie sich aber nicht befinden.
    "height":  60
  },
     
  { "x":      585,
    "y":     170,
    "vy":    150,
    "ay":    1000,
    "width":  10,
    "height":  60
  }
]
</source>


===Vögel durch einen Ball ersetzen===
Und in das Viewobjekt fügen Sie '''eine''' View-Konfiguration ein, die für beide Paddle verwendet werden kann.
 
<source lang="javascript">
Als erstes sollten Sie die Zahl der Vögel reduzieren, indem Sie einfach in der Konfigurationsdatei <code>json/config01.json</code>
"paddle":
den Counter <code>@count</code> in <code>model.bird</code> entfernen. Außerdem sollten Sie den String
{ "border": 0,
<code>"birds"</code> durch <code>"ball"</code> ersetzen und die Array-Klammer <code>[ ]</code> um das Huhn-Model-Konfigurationsobjekt entfernen.
   "color":  { "color": "#999999" }
Letzteres ist wichtig, da die Logik – um eine neue Spielrunde starten zu können – direkt, {{dh}} per Namen auf das Ball-Objekt zugreifen wird, sobald es die Bühne verlässt. Ein namentlicher Zugriff auf Array-Objekte wird vom Game-Modul (noch) nicht unterstützt.
 
(Funktioniert das Spiel noch? Es sollte nur noch ein Huhn herumflattern.)
 
Ersetzen Sie jetzt das animierte Huhn durch einen farbigen Kreis.
Ein Beispiel für eine zugehörige View-Konfiguration finden Sie in der [https://glossar.hs-augsburg.de/beispiel/tutorium/es6/ball/WK_Ball03/src/json/config04a.json Konfigurations-Datei] der
[https://glossar.hs-augsburg.de/beispiel/tutorium/es6/ball/WK_Ball03/web/index04a.html Musterlösung Ball03 (04a)]: <code>view.orange</code> oder <code>view.blue</code>.
 
Ersetzen Sie in Ihrer Konfigurationsdatei <code>view.hen</code> durch eine der beiden Graphics-Konfigurationen
und ersetzen Sie den View-Bezeichner <code>"hen"</code> unter <code>model.ball.view</code> durch
den von Ihnen gewählten View-Bezeichner.
 
Wenn Sie jetzt das Programm laufen lassen, fliegt kein Ball mehr über die Bühne.
In der Browser-Konsole wird folgender Fehler angezeigt:
 
<source lang ="javascript">
v_json_map[c_config_view.class] is not a constructor
</source>
 
Das liegt daran, dass Sie in der Konfigurations-Datei angegeben haben,
dass die Klasse <code>ViewGraphicsCircle</code> (aus <code>wk/view/ViewGraphicsCircle</code>) zum Rendern der
View verwendet werden soll. Das Huhn wurde dagegen mit <code>ViewAnimatedBird</code>
gerendert.
 
Das Game-Modul <code>game/game01.js</code> kann grundsätzlich jede beliebige Klasse zum Rendern verwenden.
Allerdings muss es diese importieren und unter dem Namen, der in der Konfigurationsdatei verwendet wird,
in der Hashmap <code>v_json_map</code> einfügen.
 
Verbessern Sie <code>game/game01.js</code> entsprechend.
 
Wenn alles funktioniert, können Sie den Import der Klasse <code>ViewAnimatedBird</code> aus
<code>game/game01.js</code> entfernen (<code>v_json_map</code> nicht vergessen). Die Datei
<code>view/ViewAnimatedBird.js</code> können Sie löschen. Sie wird nicht mehr benötigt.
 
===Feste Bühnengröße===
 
Definieren Sie in der Konfigurationsdatei eine feste Bühnengröße ({{zB}} Breite: 600 Pixel, Höhe: 400 Pixel)
'''Achtung''': Geben Sie der Konfigurationsdatei jeweils eine Integerzahl und keinen String an!
Das Hintergrundbild ist nicht sehr passend für Pong. Wenn Sie ein besseres haben, ersetzen Sie es (in der Datei <code>src/js/app01.js</code>),
ansonsten löschen Sie in der Konfigurationsdatei einfach das Attribut <code>model.stage.view</code>.
 
Legen Sie die Startposition des Balls in die Mitte der Bühne und sorgen Sie dafür, dass er einen festen Radius hat.
Die Geschwindigkeit sollte weiterhin zufällig gewählt werden. Allerdings werden Sie später vermutlich den
Geschwindigkeitsbereich anpassen.
 
===Die Schläger===
[[Datei:WK Pong01 ClassModel01 paddle.png|gerahmt|ohne|Paddle-Module]]
 
Definieren Sie die Klasse <code>MovablePaddle</code> (<code>./model/MovablePaddle.js</code>).
Sie erbt alle Attribute von <code>MovableRectangle</code> (<code>wk/model/MovableRectangle</code>)
und enthält zunächst nur einen Konstruktor, der genauso definiert wird, wie in <code>wk/model/MovableRectangle</code>.
 
Ein Schläger wird vom Spieler gesteuert. Er kann ihn (indem er auf der Tastatur entsprechende Tasten drückt) nach unten
oder oben bewegen und er kann ihn wieder anhalten (indem er die jeweilige Steuertaste wieder loslässt).
Ein Schläger hat neben den üblichen Attributen zwei weitere Attribute <code>vyMoving</code> und
<code>ayMoving</code>, die festlegen, mit welcher Geschwindigkeit sich der Schläger in y-Richtung bewegen soll,
solange der Spieler die entsprechende Taste bewegt. Schreiben Sie drei Methoden <code>down</code>,
<code>up</code> und </code>stop</code>, die dafür sorgen, dass sich der Schläger in die
richtige Richtung bzw. gar nicht bewegt.
 
<source lang ="javascript">
down()
{ if (this.vy === 0)
   { this.vy = this.vyMoving;
    this.ay = this.ayMoving;
  }
}
}
</source>
Die Methode <code>up</code> wird analog mit umgekehrten Vorzeichen definiert und
die Methode <code>stop</code> setzt die Geschwindigkeit und die Beschleunigung auf Null.
Importieren Sie <code>MovablePaddle</code> in <code>game/game01.js</code> und vergessen
Sie nicht, die Klasse auch in <code>v_json_map</code> einzutragen. Und weil Sie schon gerade dabei
sind, importieren Sie auch noch die Klasse <code>ViewGraphicsRectangle</code> aus der <code>wk</code>-Library.
Wenn Sie jetzt die Anwendung starten, wird diese Klassen noch nicht verwendet, aber
es ist zumindest sichergestellt, dass sie gefunden werden.
Legen Sie jetzt in der Konfigurationsdatei die Modell zweier Paddles samt zugehöriger View an
(wenn beide Paddle gleich aussehen sollen, dann reicht ein View-Objekt, das von beiden Paddle-Modellen referenziert wird).
Als Klasse für die View verwenden Sie <code>ViewGraphicsRectangle</code>.  Diese View Objekte werden genauso
konfiguriert, wie <code>ViewGraphicsCircle</code>-Objekte.
Die Konfiguration der Paddle-Models ist etwas raffinierter. Bei den Paddles handelt es sich um schlanke (Breite), längliche (Höhe)
Rechtecke , die jeweils parallel zu einer der senkrechten Seiten der Bühne stehen. Setzen Sie den Anchor des linken Paddles in die
Mitte der linken Seite des Rechtecks (<code>xAnchor: 0.0, yAnchor: 0.5</code>) und den Anchor des rechten Paddles
in die Mitte der rechten Seite des Rechtecks (<code>xAnchor: 1.0, yAnchor: 0.5</code>).
Das Rechteck (genauer seinen Ankerpunkt) sollten Sie jeweils 5 Pixel vom Bühnenrand entfernt mittig platzieren
(vgl. [https://glossar.hs-augsburg.de/beispiel/tutorium/es6/pong/WK_Pong01/web/index01.html Musterlösung]).
Für die Geschwindigkeitsattribute, die von den zuvor definierten Methoden verwendet werden, sollten Sie
zunächst folgende Werte verwenden:
<source lang ="javascript">
"vyMoving": 150,
"ayMoving": 500
</source>
Damit wird erreicht, dass der Schläger sich beim Start zunächst langsam bewegt, aber relativ schnell beschleunigt.
Später können Sie geeignetere Werte durch Experimentieren ermitteln.
Wenn Sie die Klasse <code>MovablePaddle</code> korrekt implementiert, die beiden Klassen
<code>MovablePaddle</code> und <code>ViewGraphicsRectangle</code> korrekt in das Game-Modul importiert
und die beiden Paddle samt View korrekt konfiguriert haben, sollten sich bei einem Start der App drei Element auf der Bühne
befinden: ein Ball (der sich über die Bühne bewegt) und zwei Schläger (die sich nicht bewegen).
Testhalber können Sie in der Konfigurationsdatei einem Schläger mal eine Geschwindigkeit in y-Richtung zuordnen.
Sie werden feststellen, dass er sich permanent zwischen unterem und oberen Rand hin und her bewegt, ohne sich vom
vertikalen Bühnenrand wegzubewegen.
===Kollisionserkennung und -behandlung===
[[Datei:WK Pong01 ClassModel01 model_update.png|gerahmt|ohne|Model-Update-Module]]
Es ist an der Zeit die Kollisionserkennung und -behandlung anzugehen.
Es gibt drei unterschiedliche Fälle:
* Schläger kollidiert mit der Wand: Falls der Schläger mit der Wand kollidiert, wird er mittels der zuvor implementierten Methode <code>stop</code> angehalten (und zurück auf die Bühne geschoben, falls er in die Wand eingedrungen ist). Es ist dann die Aufgabe des Spielers ihn mittels Tastatursteuerung in Gegenrichtung wieder in Bewegung zu setzen.
* Ball kollidiert mit Schläger: Der Ball prallt am Schläger ab. Das funktioniert im Prinzip genauso, wie das Abprallen von der Wand: Bei einer Kollision wird die x-Geschwindigkeit negiert.
* Ball kollidiert mit der Wand: Falls der Ball mit einer horizontalen Wand kollidiert, prallt er ab, wie bisher auch. Falls er mit einer senkrechten Wand kollidiert (rechte Seite des Balls kleiner linkem Rand der Bühne oder linke Seite des Balls größer rechtem Rand der Bühne!), wird die Logik informiert, dass der Ball von der Bühne verschwunden ist.
Insgesamt sind bei jeder Neuberechnung der Modellwelt fünf Fälle zu überprüfen:
* Kollision vom Ball mit der Bühne
* Kollision von Schläger1 mit der Bühne
* Kollision von Schläger2 mit der Bühne
* Kollision von Schläger1 mit dem Ball
* Kollision von Schläger2 mit dem Ball
Legen Sie zunächst drei Dateien an:
* <code>model/CollisionStageBall.js</code>
* <code>model/CollisionStagePaddle.js</code>
* <code>model/CollisionBallPaddle.js</code>
Fügen Sie jeweils eine Funktion (mit leerem Rumpf ein), die Sie exportieren:
* <code>collision(p_stage, p_ball)</code>
* <code>collision(p_stage, p_paddle)</code>
* <code>collision(p_ball, p_paddle)</code>
Importieren Sie diese Funktionen in die Datei  <code>model/ModelUpdate.js</code>
und ersetzen Sie den Befehl <code>collision(p_immovables, p_movables);</code>
durch folgende fünf Befehle:
<source lang ="javascript">
collisionStageBall  (p_models.stage, p_models.ball);
collisionStagePaddle(p_models.stage, p_models.paddle1);
collisionStagePaddle(p_models.stage, p_models.paddle2);
collisionBallPaddle (p_models.ball,  p_models.paddle1);
collisionBallPaddle (p_models.ball,  p_models.paddle2);
</source>
</source>


Beachten Sie, dass die Funktion <code>modelUpdate</code> im Parameter <code>p_models</code>
Wenn Sie keinen Syntaxfehler gemacht haben, lässt sich die <code>app01</code> immer noch fehlerfrei übersetzen.
auf alle Modelle, die in der Konfigurations-Datei im Objekt <code>model</code> definiert wurden, unter
dem  jeweiligen Konfigurationsnamen zugreifen kann. In der Musterlösung heißen die vier benötigten
Objekte <code>stage</code>, <code>ball</code>, <code>paddle1</code> und <code>paddle2</code>.
Wenn Sie Ihre Modell-Objekte in der Konfigurationsdatei anders benannt haben, müssen Sie die obigen
Befehle entsprechend anpassen.


''Anmerkung: Das Modul <code>collision.js</code> wird hier nicht verwendet. Es wäre sauberer, dieses Modul so zu verallgemeinern, dass es abhängig von den Klassen der zu testenden Objekte die jeweilige Kollisionsmethode aufrufen würde. Bei vielen Objekten, die kollidieren können, ist das besser, als Dutzende Einzelbefehle zur Kollisionserkennung zu schreiben. Aber hier würde das zu weit führen. Den Import-Befehl''
Nun ist es an der Zeit die Datei <code>app.js</code> anzupassen:
 
* Importieren Sie die beiden neu erstellten Klassen <code>ModelRectangle</code> und <code>ViewRectangleGraphics</code> analog zu <code>ModelCircle</code> und <code>ViewCircleGraphics</code>. Auch hier gilt: Wenn Sie in beiden Dateien keine Syntaxfehler gemacht haben, lässt sich die App immer noch fehlerfrei übersetzen.
<source lang ="javascript">
* Fügen Sie nach der Konstanten <code>c_config_view_ball</code> zwei Konstanten ein, mit denen Sie auf die beiden Konfigurationsobjekte zugreifen können, die Sie zuvor in die Datei <code>config.json</code> eingefügt haben:
import collision from './collision.js';
<div style="margin-left: 1.5em;"><source lang="javascript">
c_config_models_paddle = c_config_model.paddles,
c_config_view_paddle  = c_config_view.paddle,
</source>
</source>
*  Nun müssen Sie im Anschluss an die Erzeugung von <code>c_model_ball</code> 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):
<source lang="javascript">
c_models_paddle =
  [ new ModelRectangle(c_config_models_paddle[0]),
    new ModelRectangle(c_config_models_paddle[1])
  ]
</source></div>
* Als nächstes müssen Sie  das soeben erzeugte Array in das Konfigurationsobjekt vom Funktionsaufruf der Funktion <code>initUpdater</code> einfügen, da der Updater natürlich auch die Positionen der Schläger regelmäßig neu berechnen muss:
<div style="margin-left: 1.5em;"><source lang="javascript">
initUpdater({stage:  c_model_stage,
            ball:    c_model_ball,
            paddles: c_models_paddle
          });
</source></div>
* 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):
<div style="margin-left: 1.5em;"><source lang="javascript">
c_views_paddle =
[ new ViewRectangleGraphics
      (c_pixi_app, c_models_paddle[0],c_config_view_paddle),
  new ViewRectangleGraphics
      (c_pixi_app, c_models_paddle[1], c_config_view_paddle)
]
</source></div>
* Diese Views müssen vom Renderer regelmäßig neu gezeichnet werden (hier kommt wieder ES-6-Destructuring-Syntax zum Einsatz, um den Inhalt des Arrays <code>c_views_paddle</code> in ein anderes Array einzugügen; in ES 5 müssten Sie  <code>initRenderer([c_view_ball].concat(c_views_paddle));</code> schreiben)
<div style="margin-left: 1.5em;"><source lang="javascript">
initRenderer([c_view_ball, ...c_views_paddle]);
</source></div>
*Jetzt sollte die Anwendung wieder laufen und die beiden Schläger sollten zu sehen sein.


''sollten Sie daher aus der Datei <code>modelUpdate.js</code> löschen.''
====Modul <code>update</code>====
 
Sie sollten auch noch das Update-Modul <code>update</code> aktualisieren. Die Schläger bewegen sie noch nicht,
Implementieren Sie nun die drei Methoden.
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.


'''Kollision vom Ball mit der Bühne'''
* Fügen Sie den den Parameter <code>paddles: p_paddles</code> in das Config-Objekt der Parameterliste der Funktion <code>initUpdater</code> ein.
* Speichern Sie das im Parameter <code>p_paddles</code> übergebene Array mit den <code>ModelRectangle</code>-Objekten in einer globalen (aber privaten) Variablen <code>v_paddles</code> des Moduls.
* Fügen Sie in die Methode <code>update</code> eine For-Loop ein, die für jedes Objekt im Array <code>v_paddles</code> 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:<br/>https://glossar.hs-augsburg.de/beispiel/tutorium/2018/pong/WK_Pong01/web/index01.html


Das ist zunächst ganz einfach. Kopieren Sie den Code aus <code>wk/model/collisionBB</code> und entfernen Sie die Tests
===Aufgabe 2===
für den linken und den rechten Bühnenrand. Später fügen Sie noch Code ein, der die Logik-Komponente informiert, wenn der Ball die
(Musterlösung: [https://gitlab.multimedia.hs-augsburg.de/kowa/WK_Pong01.git Gitlab: <code>WK_Pong01</code>, <code>app02</code>], [https://glossar.hs-augsburg.de/beispiel/tutorium/2018/pong/WK_Pong01/web/index02.html index02.html])
Bühne verlässt.


Wenn Sie das Programm mehrfach starten, sollte jetzt der Ball vom oberen und unteren Rand abprallen, aber rechts und links von der Bühne verschwinden.
Sorgen Sie jetzt dafür, dass die Kollisionen der Paddle mit dem Ball und der Bühne erkannt und behandelt werden.


'''Kollision vom Schläger mit der Bühne'''
* Fügen Sie in die Klassen <code>ModelCircle</code> und <code>ModelRectangle</code> Setter-Methoden für die Attribute <code>left</code>, <code>right</code>, <code>top</code> und <code>bottom</code> 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 gewünschten Wert als Ergebnis liefert.
 
* Definieren Sie die Klasse <code>ModelPaddle</code> als Subklasse der Klasse <code>ModelRectangle</code>. Der Konstruktor <code>constructor(p_config)</code> übergibt das Konfigurationsobjekt <code>p_config</code> einfach an seine Superklasse: <code>super(p_config);</code>. Überschreiben Sie anschließend die Resetfunktion. Diese soll die Breite, Höhe und Position des Schlägers genauso wie die Klasse <code>ModelRectangle</code> auf die im Konfigurationsobjekt übergebenen Werte zurücksetzen.  Die Geschwindigkeit und die Beschleunigung soll sie allerdings jeweils auf <code>0</code> setzen, da die Schläger sich zu Beginn nicht bewegen.
Dieser Test ist noch einfacher. Wenn der obere Rand des Schlägers kleiner oder gleich dem oberen Rand der Bühne ist,  
* Verwenden Sie im Modul <code>app</code> die Klasse <code>ModelPaddle</code> anstelle der Klasse <code>ModelRectangle</code> zum Erzeugen der beiden Schläger-Models.
wird der Schläger auf die Bühne zurück geschoben (<code>p_paddle.top = p_stage.top;</code>) und angehalten <code>p_paddle.stop();</code>.
* Fügen Sie in die Klasse  <code>ModelPaddle</code> drei Methoden <code>down</code>, <code>up</code> und <code>stop</code> ein.
Die Kollisionserkennung mit dem unteren Rand erfolgt auf die gleiche Art und Weise.
** Die Methode <code>down</code> setzt die Geschwindigkeit und die Beschleunigung in y-Richtung auf die im Konfigurationsobjekt übergebenen Werte, '''sofern die Geschwindigkeit beim Aufruf gleich <code>0</code> ist'''.
 
** Die Methode <code>up</code> 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 <code>0</code> ist'''.
'''Kollision von Ball und Schläger'''
** Die Methode <code>stop</code> setzt die Geschwindigkeit und die Beschleunigung in y-Richtung auf <code>0</code>, '''sofern die Geschwindigkeit beim Aufruf ungleich <code>0</code> ist'''.
 
* Definieren Sie eine Modul <code>collisionPaddleStage(p_paddle, p_stage)</code>, das das übergebene Paddle mittels der zuvor definierten Methode <code>stop</code> 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: <code>p_paddle.top = p_stage.top;</code> bzw. <code>p_paddle.bottom = p_stage.bottom;</code>.
Das ist eine sehr aufwändige Operation, da 8 Fälle unterschieden werden müssten: Kollision
* Wenn Sie möchten, können Sie auch noch gleich die Kollisionserkennung und -behandlung von Kreis und Bühne (<code>collisionCircleStage</code>) vereinfachen, indem Sie die zuvor definierten Attribute <code>left</code>, <code>right</code> ... verwenden.
des Balls mit einer der vier Seiten oder einer der vier Ecken. Folgender recht grober Test
* Schreiben Sie eine Kollisionererkennung- und behandlung <code>collisionPaddleCircle(p_paddle, p_circle)</code> 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:
muss daher vorerst reichen (der Ball verhält sich allerdings ziemlich eigenartig, wenn
<div style="margin-left: 1.5em;"><source lang="javascript">
er mit einer der Ecke oder Schmalseite des Schlägers kollidiert);
function collisionPaddleCircle(p_paddle, p_circle)
 
{ if (p_circle.y + 0.8*p_circle.r >= p_paddle.top &&
<source lang ="javascript">
      p_circle.y - 0.8*p_circle.r <= p_paddle.bottom
if (p_ball.y + 0.5*p_ball.r >= p_paddle.top &&
    p_ball.y - 0.5*p_ball.r <= p_paddle.btm
  )
{
  if (p_ball.vx > 0 &&  // The ball is moving from left to right.
      p_ball.rgt >= p_paddle.lft && p_ball.lft < p_paddle.rgt
     )
     )
   { p_ball.rgt = p_paddle.lft;
   { if (p_circle.vx > 0 &&  // The ball is moving from left to right.
    p_ball.vx = -p_ball.vx;
        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_ball.vx < 0 &&  // The ball is moving from right to left.
    if (p_circle.vx < 0 &&  // The ball is moving from right to left.
      p_ball.lft <= p_paddle.rgt && p_ball.rgt > p_paddle.lft
        p_circle.left <= p_paddle.right && p_circle.right > p_paddle.left
    )
      )
  { p_ball.lft = p_paddle.rgt;
    { p_circle.left = p_paddle.right;
    p_ball.vx = -p_ball.vx;
      p_circle.vx   = -p_circle.vx;
    }
   }
   }
}
}
</source>
</source></div>
 
* Importieren Sie die beiden Funktionen <code>collisionPaddleStage</code> und <code>collisionPaddleCircle</code> und das Update-Modul und runfen Sie die beiden Funktionen innerhalb der Schleife der Update-Funktion geeignet auf.
Testen Sie Ihre Kollisionsfunktionen, indem Sie in der Konfigurationsdatei unterschiedliche
Parameter eingeben: Startposition, Startgeschwindigkeit, Größe des Schlägers, evtl. auch unterschiedliche Geschwindigkeiten des Balls.
 
===Spielsteuerung===
[[Datei:WK Pong01 ClassModel01 control logic.png|gerahmt|ohne|Steuerungsmodule]]
 
Als nächstes kommt die Spielsteuerung an die Reihe. Die beiden Spieler sollen die der Lage sein,
jeweils einen Schläger mittels Tastatur zu bewegen.
 
Zum Einsatz kommt hier das [[Observer-Pattern]]: Jedes Mal, wenn ein spezifischer Tastendruck erfolgt,
schickt der Controller <code>PaddleControl</code> eine entsprechende Nachricht an alle Listener. Das
Logik-Modul registriert sich als Listener für derartige Nachrichten, empfängt diese daher künftig und
reagiert auf jede Nachricht, die es erhält, in geeigneter Form.  
 
Sehen Sie sich zunächst einmal die die ursprüngliche Moorhuhn-Anwendung (app00) an.
Dort gibt es einen Controller, der bei einem Klick auf einen
Vogel die Logik darüber informiert. Die Logik reagiert auf eine entsprechende Nachricht, indem sie
den zugehörigen Vogel von der Bühne löscht (mittels der Remove-Funktion, die die Logik vom
Game-Modul erhalten hat).
Die Verknüpfung des Controllers mit den Vögeln erfolgt über das Game-Modul:
In der Konfigurations-Datei <code>config00.json</code> gibt es das Attribut
<code>view.hen.control</code>. Mit diesem Attribut wird der Moorhuhn-View
die Klasse <code>controlBird</code> zugeordnet. Wie üblich wird diese Klasse im Game-Modul
importiert und in der Hashmap <code>v_json_map</code> eingetragen.
 
Entfernen Sie diesen Controller zunächst vollständig (bevor Sie mit der Implementierung der
Tastatur-Controller anfangen):
 
* Entfernen Sie – sofern Sie dies noch nicht gemacht haben – das Objekt <code>view.hen</code> aus der Konfigurationsdatei <code>controll01.json</code> (und nicht etwa aus der JSON-Datei  <code>config00.json</code>, die Sie zuvor analysiert haben)
* Benennen Sie die Datei <code>control/controlBird.js</code> in <code>control/controlPaddle.js</code> um. Benennen Sie die darin enthaltene Funktion ebenfalls um. Als Parameter wird dieser Controller ein Konfigurationsobjekt <code>p_config</code> erhalten. Den Rumpf der Funktion löschen Sie einfach.
* Ändern Sie den Bezeichner <code>controlBird</code> in der Datei <code>game.js</code> geeignet ab.
* Löschen sie in der Datei <code>logic/logic.js</code> den Inhalt des Rumpf der Funktion <code>init</code>.
Jetzt sollte die Anwendung wieder laufen, ohne dass sich hinsichtlich der Behandlung von Benutzereingaben irgendetwas geändert hätte.
Sie haben ja in dieser Hinsicht auch noch nichts implementiert, sondern nur überflüssigen Ballast entfernt.
 
Dies folgt als nächster Schritt.
 
In der Anwendung <code>app00</code> wurden der Controller den View-Objekten
der Moorhühner zugeordnet, da ein Klick auf ein Moorhuhn eine Reaktion des Spiel auslösen sollte.
Diesmal soll der Controller die Tastatur überwachen. Da es sich für die Tastatur kein
View-Objekt gibt, muss die Konfiguration diesmal etwas anders erfolgen.
Das Game-Modul <code>game.js</code> wurde so implementiert, dass es neben der
Erzeugung und Initialisierung von Model- und View-Objekten auch die Initialisierung von Control-Objekten
übernehmen kann.
 
Fügen Sie in die Datei <code>config01.json</code> hinder dem Objekt <code>view</code>
ein Objekt namens <code>control</code> ein. (Sie könnten es auch vor diesem Objekt oder sogar
vor dem Objekt <code>model</code> einfügen. Die Reihenfolge der Attribute eine Objektes spielt keine Rolle
– zumindest sollte sie keine Rolle spielen.) In das Objekt <code>control</code> fügen Sie zwei Objekte
<code>paddle1</code> und <code>paddle2</code> zur Konfiguration der Steuerung der beiden Schläger ein.
 
Jedes dieser beiden Objekte enthält vier Attribute:
 
* <code>control</code>: Hier muss der Name der zugehörigen Controller-Funktion angegeben werden. Im Fall der beiden Schläger ist dies die (bereits existierende aber noch nicht implementierte) Funktion <code>controlPaddle</code>.
* <code>model</code>: Hier muss der Name des zu kontrollierenden Models angegeben werden. Im Model-Objekt weiter oben in der Konfigurationsdatei hatten Sie die Konfigurationen für zwei Schläger angelegt. Geben Sie im Controller jeweils den Namen eines dieser beiden Objekte an.
* <code>up</code> und </code>down</code>: Hier muss jeweils ein Objekt angegeben werden, die eine Keyboard-Taste beschreibt. Beispielsweise beschreibt das Objekt <source lang="javascript">
{"key": "ArrowUp",  "keyCode": 38}</source> die Pfeil-nach-oben-Taste. (Leider ist es derzeit noch notwendig sowohl den Namen einer Taste, als auch den zugehörigen Key-Code anzugeben, da beispielsweise Android-Geräte mit (Bluetooth-)Tastatur die modernere Key-Variante noch nicht beherrschen.<ref>[https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key MDN web docs – KeyboardEvent.key]</ref>). Allerdings ist die Verwendung von Key-Codes als ''deprecated'' (''angelehnt''/''überholt'') markiert worden, das die Codes browser- und tastaturabhängig sind (siehe {{zB}} [https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode MDN web docs – KeyboardEvent.keyCode]<ref> [https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode MDN web docs – KeyboardEvent.keyCode]</ref>). Man sollte also bei einem Tastaturevent immer erst überprüfen, ober der Bezeichner der Taste den Vorgaben entspricht, bevor man auf den Key-Code zurückgreift. '''Tipp:''' Auf der Seite http://keycode.info/ können Sie eine beliebige Taste drücken und erhalten beides <code>keyCode</code> und <code>key</code>.
 
Sobald Sie die beiden Controller konfiguriert haben, sollten Sie Ihre Anwendung testen. Sie sollte fehlerfrei durchlaufen, aber auf Tastaturereignisse reagiert sie noch nicht.
Es ist aber schon gut zu wissen, dass der Programmstart keinen Fehler zur Folge hat. Wenn sie das Modul <code>controlPaddle</code> nicht sauber ins Game-Modul
integriert haben, erhalten Sie einen Fehler: <code>v_json_map[l_config.control] is not a function</code>. Probieren Sie es aus, indem Sie in der Datei
<code>config01.json</code>  einmal absichtlich einen Schreibfehler in das Wort <code>controlPaddle</code> einfügen.  


Jetzt ist es an der Zeit, die Funktion <code>controlPaddle</code> (in der Datei <code>control/controlPaddle.js</code>) zu implementieren.
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
Schreiben Sie testhalber mal den Befehl
({{dh}} nach dem Start der Game Loop) folgenden Befehl in den Rumpf der Init-Funktion im Modul <code>app</code> einfügen:


<source lang="javascript">
<source lang="javascript">
console.log(p_config);  
c_models_paddle[1].up();
</source>
</source>
 
Damit sollte sich – sofern Sie Ihr Array mit den beiden Schläger-Models <code>c_models_paddle</code>
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:
<br/>https://glossar.hs-augsburg.de/beispiel/tutorium/2018/pong/WK_Pong01/web/index02.html


in den Rumpf dieser Funktion. Im Konsolfenster des Browsers sollten beim Programmstart die beiden zuvor erstellten Konfigurationsobjekte ausgegeben werden.
Sie sollten testweise auch den anderen Schläger nach oben oder unten bewegen.


Wie man ein Tastaturereignis abfängt (mit <code>window.addEventListener()...</code>) und behandelt (mittels Callback-Funktion)
===Aufgabe 3===
sollte Ihnen bekannt sein: [[HTML5-Tutorium: JavaScript: Hello World 03]], insbesondere Datei <code>[https://glossar.hs-augsburg.de/beispiel/tutorium/es6/hello_world/WK_HelloWorld03/main.js main.js]</code>. 
(Musterlösung: [https://gitlab.multimedia.hs-augsburg.de/kowa/WK_Pong01.git Gitlab: <code>WK_Pong01</code>, <code>app03</code>], [https://glossar.hs-augsburg.de/beispiel/tutorium/2018/pong/WK_Pong01/web/index03.html index03.html])


Fügen Sie '''in''' den Rumpf der Funktion <code>controlPaddle</code> zwei Funktionen <code>o_start_moving</code> und <code>o_stop_moving</code>
Als nächstes sollten Sie eine Contoller für die Schäger einbauen:
(Präfix <code>o_</code> da es sich um [[Observer]]-Funktionen handelt; observer = event listener).
jeweils mit Parameter <code>p_event</code> ein. Sorgen Sie dann mittels geeigneter <code>window.addEventistener</code>-Anweisungen dafür, dass die
erste Funktion bei einem <code>keyDown</code>-Event ausgeführt wird und die zweite bei einem <code>keyUp</code>-Event. Bei jedem Aufruf soll
die zugehörige Observer-Funktion ihren eigenen Namen sowie den Key-Bezeichner und den Key-Code der Taste ausgeben, die den Event veranlasst hat.


Wenn alles wunschgemäß funktioniert, sollten Sie jetzt abhängig von dem jeweiligen Tastendruck eine geeignete Nachricht per Event-Dispatcher verschicken.
* Fügen Sie in die Konfigurationsdatei nach <code>"model"</code> und <code>"view"</code> ein drittes Objekt ein, das beschreibt, mit welchen Tasten die beiden Schläger gesteuert werden sollen:
Löschen sie in der Datei <code>control/control.js</code> die überflüssige „Konstante“ <code>control.C_EVENT_BIRD_HIT</code> und fügen Sie  
<div style="margin-left: 1.5em;"><source lang="javascript">
dafür für die drei möglichen Schläger-Ereignisse drei neue Konstanten ein:
"control":
{ "paddles":
  [ { "up":  "w",
      "down": "x"
    },
    { "up":  "ArrowUp",
      "down": "ArrowDown"
    }
  ]
}
</source></div>
* Legen Sie eine neue Datei <code>control/ControlPaddle.js</code> an, die einen Klasse <code>ControlPaddle</code> definiert und eportiert. Diese Klasse enthält lediglich einen Kostruktor. Dieser definiert zwei interne Funktionen <code>o_start_moving</code> und <code>o_stop_moving</code> (<code>o_</code> steht für [[Observer Pattern '''O'''bserver]]), die als Event Handler für Tastaturereignisse verwendet werden (vgl. [[HTML5-Tutorium:_JavaScript:_Hello_World_03#Barrierefreiheit| Hello-World-Tutorium, Teil 3, Aufgabe 6]]). Wenn eine der beiden Tasten im Konfigurationsobjekt '''gedrückt''' werden (<code>window</code>-<code>keydown</code>-Ereignis), wird der Schläger mittels einer der zuvor definierten Methoden <code>up</code> oder </code>down</code> n die gewünschte Richtung bewegt. Sobald die Taste wieder losgelassen wird (<code>window</code>-<code>keyup</code>-Ereignis) wird der Schläger mittels der Methode <code>stop</code> wieder angehalten.
<div style="margin-left: 1.5em;"><source lang="javascript">
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);
}
</source></div>
* Nun müssen Sie noch diController für die beiden Schläger im Modul <code>app</code> erstellen und initialisieren:
** Importieren Sie zunächst die neu definierte Klasse <code>ControlPaddle</code>.  
** Definieren Sie dann analog zu den anderen Konstanten, in denen Sie bestimmte Teilobjekte der Konfigurationsdatei speichern, zwei Konstanten <code>c_config_control = config.control</code> sowie <code>c_config_control_paddles = c_config_control.paddles</code>. In der zweiten Konstante wird das Konfigurationsobjekt für den Paddle-Controller gespeichert, das Sie zuvor in die Datei <code>config.json</code> 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.
<div style="margin-left: 3em;"><source lang="javascript">
new ControlPaddle(c_models_paddle[0], c_config_control_paddles[0]);
new ControlPaddle(c_models_paddle[1], c_config_control_paddles[1]);
</source></div>


<source lang="javascript">
Wenn Sie jetzt die Anwendung starten, sollten Sie die beiden Schläger mittels der Tasten <code>w</code> und <code>x</code> bzw. <code>ArrowUp</code> und <code>ArrowDown</code> bewegen können. Der Ball sollte abprallen, wenn er mit einem kollidiert:
control.C_EVENT_PADDLE_UP  = 'EventPaddleUp';
<br/>https://glossar.hs-augsburg.de/beispiel/tutorium/2018/pong/WK_Pong01/web/index03.html
control.C_EVENT_PADDLE_DOWN = 'EventPaddleDown';
control.C_EVENT_PADDLE_STOP = 'EventPaddleStop';
</source>


Gehen Sie zurück in die Datei <code>control/controlPaddle.js</code>.
===Aufgabe 4===
Dort sehen Sie, dass das Event-Dispatcher-Objekt <code>control</code>, für das Sie gerade ein paar Konstanten definiert haben,
(Musterlösung: [https://gitlab.multimedia.hs-augsburg.de/kowa/WK_Pong01.git Gitlab: <code>WK_Pong01</code>, <code>app04</code>], [https://glossar.hs-augsburg.de/beispiel/tutorium/2018/pong/WK_Pong01/web/index04.html index04.html])
bereits importiert wird. Sie können dieses Objekt verwenden, um Nachrichten an 
andere interessierte Objekte zu verschicken.


Fügen Sie an den Anfang des Rumpfs der Funktion <code>controlPaddle</code> ein paar Konstantendefinitionen ein, die die Attribute des Konfigurationsobjektes
Nun ist es an der Zeit, die Spiellogik zu implementieren.
speichern:


* Fügen Sie ein paar neue Informationen in die Konfigurationsdatei ein:
<div style="margin-left: 1.5em;"><code>"model"</code>: Hier werden Informationen über die beiden Score-Textfelder erfasst: Position und Startwert:
<source lang="javascript">
<source lang="javascript">
const
"scores":
  c_model_name  = p_config.model,
[ { "x":    20,
   c_key_up      = p_config.up.key,
    "y":    20,
   c_keycode_up  = p_config.up.keyCode,
    "score":  0
 
   },
  c_key_down     = p_config.down.key,
   { "x":    580,
   c_keycode_down = p_config.down.keyCode;
    "y":    20,
     "score":  0
   }
]
</source>
</source>
 
<code>"view"</code>: Hier wird festgelegt, wie eine (Score-)Textfeld formartiert (<code>"style"</code>) und positioniert (<code>anchor</code>; <code>0.5</code> entspricht zentriert) werden soll:
Definieren Sie im Anschluss an die Konstanten eine Hilfsfunktion zum Dispatchen eines neuen Events:
 
<source lang="javascript">
<source lang="javascript">
function f_dispatch(p_type)
"text":
{ control.dispatchEvent(new CustomEvent(p_type, {'detail': c_model_name})); }
{ "style": { "fontFamily": ["Verdana", "Helvetica", "sans-serif"],
            "fontSize":  "20px"
          },
  "anchor": {"x": 0.5, "y": 0}
}
</source>
</source>
 
<code>"logic"</code>: Ein neuer Eintrag an Ende der Konfigurationsdatei, in der definiert wird, wie viele Runden das Spiel dauern soll:  
Diese Funktion erwartet den Typ des Ereignisses, das verschickt werden soll, im Eingabe-Parameter <code>p_type</code>.
Prinzipiell kann jeder beliebige String verwendet werden, Sie sollten sich aber (zunächst) auf die drei zuvor definiert
„Konstanten“ <code>control.C_EVENT_PADDLE_UP</code>, <code>control.C_EVENT_PADDLE_DOWN</code>
und <code>control.C_EVENT_PADDLE_STOP</code> beschränken.
 
Die Nachricht, {{dh}} das Event-Objekt, das verschickt wird, wird mittels <code>new CustomEvent(p_type, p_init)</code>
erstellt (siehe [https://developer.mozilla.org/de/docs/Web/API/CustomEvent MDN web docs – CustomEvent] und auch [https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Creating_and_triggering_events MDN web docs – Creating and triggering events]<ref>[https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Creating_and_triggering_events MDN web docs – Creating and triggering events]</ref>).
 
Als Detail-Information, auf wen sich das Ereignis bezieht, wird der im Konfigurationsobjekt gespeicherte Name
<code>p_config.model</code> (bzw. <code>c_model_name</code>) an den Empfänger der Nachricht übermittelt.
 
Implementieren Sie die Funktion <code>o_start_moving</code> so, dass das Ereignis <code>control.C_EVENT_PADDLE_UP</code> (mittels der zuvor definierten
Hilfsfunktion) verschickt wird, wenn die im Konfigurationsobjekt angegebene „Up“-Taste betätigt wurde, und das Ereignis  code>control.C_EVENT_PADDLE_DOWN</code>,
falls die „Down“-Taste betätigt wurde. Die Funktion <code>o_stop_moving</code> soll das Ereignis <code>control.C_EVENT_PADDLE_STOP</code> versenden,
sobald eine der genannten Tasten losgelassen wird.
 
'''Tipp''': Wenn Sie vor der Funktion <code>controlPaddle</code> den folgenden Kommentar einfügen,
<source lang="javascript">
<source lang="javascript">
/**
{ "winScore": 3 }
* @param p_config {{model: String,
*                  up:    {key: String, keyCode: Number},
*                  down:  {key: String, keyCode: Number},
*                  }
*                }
*/
</source>
</source>
zeigt Ihnen WebStorm weniger Warnings an. Die Typdefinition, die in geschweiften Klammern hinter dem Parameter <code>p_config</code>
Ersetzen Sie in der Konfigurationsdatei nun noch im Ballobjekt die festen Geschwidigkeitswerte <code>"vx": 200, "vy": 150</code> durch die nachfolgenden Objekte. Diese können später in der Resetfunktion von <code>ModelCircle</code> mittels <code>concretize</code> 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 [[MMProg:_Praktikum:_WiSe_2018/19:_Ball03#Aufgabe_2:_Konfiguration_der_Anwendung_mittels_JSON|Ball03, Aufgabe 2]]):
angegeben wird, beschreibt, wie das Konfigurationsobjekt aufgebaut sein sollte. Tatsächlich enthält das Objekt, das übergeben wird noch
ein weiteres Attribut (nämlich <code>"control": "controlPaddle"</code>). Das wird aber vom Game-Modul benötigt, um die korrekte Control-Funktion aufzurufen,
die Control-Funktion selbst benötigt dieses Attribut nicht mehr.  
 
Wie können Sie nun Ihren Code testen? Ganz einfach, indem Sie zugehörige Event-Listener schreiben.
Wo dieser definiert wird, ist dem Event-Dispatcher vollkommen egal. Daher schreiben Sie die die Tests einfach gleich in das Modul
<code>controlPaddle</code> selbst hinter die Funktion <code>controlPaddle</code>.  
 
<source lang="javascript">
<source lang="javascript">
control.addEventListener
"vx": {"@min": 150, "@max": 300, "@positive": 0.5},
( control.C_EVENT_PADDLE_DOWN,
"vy": {"@min": 150, "@max": 300, "@positive": 0.5}
  function(p_event)
  { console.log('down', p_event.detail); }
);
</source>
</source>
</div>
* Damit das Spiel wieder funktioniert, müssen Sie die Klasse <code>ModelCircle</code> so erweitern, dass die Resetfunktion bei jedem Aufruf die Funktion <code>concretize</code> auf das Konfigurationsobjekt anwendet, bevor sie die Attribute des Ballobjektes auf die Initialwerte zurücksetzt. Improtieren Sie zunächst die Funktion <code>/wk/util/concretize</code> in das Modul. Anschließend müssen Sie den Konstruktor und die Resetfunktion so umschreiben, dass die Zerlegung des Konfigurationsobjektes ([[Destructuring]]) nicht mehr im Konstruktor, sondern erst in der Resetfunktion durchgeführt wird, '''nachdem <code>concretize</code> darauf agewendet wurde (dabei wird auf die Attribute <code>this.rConfig</code>, <code>this.xConfig</code> etc. verzichtet; stattdessen wird das ganze Konfigurationsobjekt in <code>this.config</code> dauerhaft gespeichert):
<div style="margin-left: 1.5em;"><source lang="javascript">
class ModelCircle
constructor(p_config = {})
{ this.config = p_config;
  this.reset();
}
 
reset()
{ const {r=0, x=0, y=0, vx=0, vy=0} = concretize({config: this.config});
  this.r  = r;
  this.x  = x;  this.y  = y;
  this.vx = vx; this.vy = vy;
}
...
</source></div>
* Jetzt sollte das Spiel wieder funktionieren, mit dem Unterschied, dass der Ball bei jedem Aufruf von der Mitte aus in eine zufällige Richtung losfliegt.
* Wenn Sie möchten, können und sollten Sie (aus Symmetriegründen) auch die Klassen <code>ModelRectangle</code> und <code>ModelPaddle</code> so umschreiben, dass das Destructuring erst in der Resetfunktion stattfindet.
* Nun müssen Sie die beiden Klassen <code>ModelScore</code> und <code>ViewText</code> zur Verwaltunge der Score-Textfelder erstellen (und im jeweiligen Modul auch exportieren!).
** Der Konstruktor von <code>ModelScore</code> hat einen Parameter <code>p_config</code> in dem ihm ein Konfigurationsobjekt übergeben wird, das zuvor in der Konfigurationsdatei definiert wurde (beispielsweise <code>{ "x": 20, "y": 20, "score": 0}</code>). Diese Objekt wird wie auch schon in der Klasse <code>ModelCircle</code> im Attribut <code>this.config</code> gespeichert und in der Resetfunktion zum Initialisieren der drei Attribute <code>this.score</code>, <code>this.x</code> und <code>this.y</code> verwendet. Auf den Einsatz der  Funktion ycodecretize</code> kann hier verzichtet werden. (Es würde aber auch keine Probleme verursachen, wenn sie dennoch aufgerufen werden würde.) 
** Neben dem Konstruktor, und der Resetfunktion gibt es noch eine Gettermethode <code>text</code>, mittels der die Renderfunktion auf den Stringwert des Scores zugreifen kann. (Im Attribut <code>this.code</code> wird ein Zahlwert gespeichert, damit dieser mittels des <code>++</code>-Operators einfach hochgezählt werden kann.
<div style="margin-left: 3em;"><source lang="javascript">
get text()
{ return this.score.toString(); }
</source></div>
* Die Klasse <code>ViewText</code> ist im Prinzip wie die Klasse <code>ViewSpriteCircle</code> aufgebaut. Die wesentlichen Unterschiede sind:
** Es wird die PixiJS-Klasse [http://pixijs.download/dev/docs/PIXI.Text.html <code>Text</code>] nstelle der Klasse <code>Sprite</code> importiert.
** Das Konfigurationsobjekt, das dem Konstruktor übergeben wird, enthält kein Attribut <code>image</code>, sondern die Attribute <code>style</code> und <code>anchor</code>.
** Im Model-Objekt wird ein Objekt übergeben, dass in einem Attribut <code>text</code> den Text bereitstellt, der visualisiertwerden soll. Die Objekte der Klasse <code>ModelScore</code> haben so ein Attribut.
** Das Sprite-Objekt wird mittels <code>const c_sprite = new Text(p_model.text, p_style)</code> erstellt, wobei <code>p_model</code> der Parameter ist, indem dem Konstruktor das Score-Model übergeben wurde, und <code>p_style</code> das gewünschte Styleobjekt aus der Konfigurationsdatei enthält. In diesem Objekt dürfen alle Styleattribute verwendet werden, die PixiJS unterstützt: {http://pixijs.download/dev/docs/PIXI.TextStyle.html PIXI.TextStyle]. Anschließend muss noch der Anchor des Textobjektes korrekt gesetzt werden: <code>c_sprite.anchor = p_anchor;</code>.
** Der Rest des Konstruktors stimmt mit dem Rest des Konstruktors der Klasse <code>ViewSpriteCircle</code> überein.
** Die Renderfunktion muss nicht nur das Text-Objekt korrekt platzieren, sondern auch noch den gewünschten Text darstellen. Fügen Sie daher den folgenden Befehl ein:
<div style="margin-left: 3em;"><source lang="javascript">
this.sprite.text = this.model.text;
</source></div>
* Erzeugen und initialisieren Sie die beiden Score-Textfelder wie üblich im App-Modul:
** <code>ModelScore</code> und <code>ViewText</code> importieren,
** <code>c_config_models_score</code> und <code>c_config_view_text</code> wie üblich definieren,
** <code>c_models_score</code> analog zu <code>c_models_paddle</code> definieren (mit <code>ModelScore</code> als Konstruktor),
** <code>c_views_score</code> analog zu <code>c_views_paddle</code> definieren (mit <code>ViewText</code> als Konstruktor).
** Fügen Sie das Array <code>c_views_score</code> analog zu <code>c_view_paddle</code> in das Array ein, das der Funktion <code>initRenderer</code> als Argument übergeben wird, damit künftig auch die Textfelder gerendert werden.
** Sie haben nun soviele Models definiert, dass es sich rentiert, eine Konstante <code>c_models</code> zu definieren, die alle Models enthält und diese Konstante sowohl an fie Funktion <code>initUpdater</code> zu übergeben (<code>initUpdater(c_models);</code>) als auch später an die Logic (sobald diese definiert ist):
<div style="margin-left: 3em;"><source lang="javascript">
c_models =
  { stage:  c_model_stage,
    ball:    c_model_ball,
    paddles: c_models_paddle,
    scores:  c_models_score
  }
</source></div>
* Als nächstes müssen Sie die Logik in der Datei <code>logic/logic.js</code> implementieren. Diese stellt wie üblich eine Funktion <code>initLogic</code> bereit, in der ihr alle wesentlichen Objekte übergeben werden, die Sie kennen muss, um das Spiel zu verwalten. Das sind die Gam Loop (da sie das Spiel starten und stoppen können sollte), die Models sowie das Konfigurationsobjekt <code>logic</code> aus der Konfigurationsdatei. Wie üblich muss diese Funktion die ihr übergebenen Werte in (dateiinternen) Variablen speichern. Anschließend sollte sie das Spiel starten:
<div style="margin-left: 1.5em;"><source lang="javascript">
function initLogic(p_game_loop,
                  {ball: p_ball, paddles: p_paddles, scores: p_scores},
                  {winScore: p_win_score = 10} = {}
                  )
{ v_game_loop = p_game_loop;
  v_ball      = p_ball;
  v_paddles  = p_paddles;
  v_scores    = p_scores;
  v_win_score = p_win_score;


(Analog für die beiden anderen Event-Typen.)
  // start the game
 
  v_game_loop.start();
====Behandlung von Tastaturevents durch die Logik====
}
Wenn alles klappt, {{dh}}, wenn Sie im Konsolfenster des Browsers korrekt benachrichtigt werden,  
</source></div>
sobald Sie der in der Konfigurationsdatei spezifizierten Tasten drücken, sollten Sie die drei Testbefehle
* Darüber hinaus muss die Logik eine Funktion implementieren, die die Kollissionserkennung aufrufen kann, wenn ein Spieler einen Ball passieren lässt. Diese muss die Punkte des Gegners erhöhen, die Game Lopp stoppen, wenn das Spiel vorbei ist oder eine neue Runde starten, wenn das Spiel noch nicht vorbei ist.
ans Ende des Rumpfes der <code>init</code>-Funktion in der Datei <code>logic/logic.js</code> einfügen.
<div style="margin-left: 1.5em;"><source lang="javascript">
function ballLoss(p_player)
{ // the score of the other player is raised
  const c_player = p_player === 'left' ? 1 : 0;
  v_scores[c_player].score++;
 
  if (v_scores[c_player].score === v_win_score) // game over
  { v_game_loop.stop();
    v_ball.reset();
  }
  else // the next round starts
  { v_ball.reset(); }
}
</source></div>
* Beide Funktionen müssen exportiert werden.
* Als nächstes müssen Sie dafür sorgen, dass die Kollisionsfunktion <code>collisionCircleStage</code> den Ball nicht mehr an den Wänden hinter den Schlägern abprallen lässt. Anstatt dessen muss der Ball die Bühne verlassen und die Logik muss informiert werden, welcher Spieler (= Schläger) den Ball verlohren hat. Improtieren Sie dazu in diesem Modul die zuvor definierte Funktion <code>ballLoss</code> aus der Logik und ersetzen Sie die bisherigen Kollsisionsabfragen für die linke und die rechte Wand durch folgenden Code:
<div style="margin-left: 1.5em;"><source lang="javascript">
if (p_circle.right < p_stage.left)
  { ballLoss('left'); }
  if (p_circle.left > p_stage.right)
  { ballLoss('right'); }
</source></div>
* Integrieren Sie die Logik nun auch noch ins Modul <code>app</code>:
** Importieren Sie die Funktion <code>initLogic</code>.
** Definieren Sie <code>c_config_logic</code> wie üblich.
** Ersetzen Sie zum Schluss den Start der Game Loop am Ende der Initfunktion durch die Initialisierung der Logik:
<div style="margin-left: 3em;"><source lang="javascript">
initLogic( new GameLoop({update: update, render: render}),
            c_models,
            c_config_logic
          );
</source></div>


Da das Logikmodul ebenfalls das Objekt <code>control</code> importiert, sollte die Anwendung immer
Wenn jetzt alles funktioniert, sollten Sie ein erstes Spiel wagen können.
noch funktionieren. Das heißt, im Konsolfenster des Browsers sollten weiterhin die Tastenereignisse protokolliert werden.
<br/>https://glossar.hs-augsburg.de/beispiel/tutorium/2018/pong/WK_Pong01/web/index04.html


Im Logikmodul haben Sie Zugriff auf die Modelle, die vom Game-Modul erzeugt wurden: <code>p_models</code>.
===Aufgabe 5===
Im Parameter <code>p_event</code> der drei Eventlistener erhalten Sie '''den Namen''' des Objektes, das mittels
(Musterlösung: [https://gitlab.multimedia.hs-augsburg.de/kowa/WK_Pong01.git Gitlab: <code>WK_Pong01</code>, <code>app05</code>], [https://glossar.hs-augsburg.de/beispiel/tutorium/2018/pong/WK_Pong01/web/index05.html index05.html])
des aktuellen Tastaturevents gesteuert werden soll: <code>p_event.detail</code>. Damit ist es möglich,
das Schläger-Model-Objekt selbst zu ermitteln: <code>p_models[p_event.detail]</code>. Für diese Objekte hatten Sie zuvor
drei Methoden definiert: <code>up</code>, <code>down</code> und <code>stop</code>.  


Rufen Sie in jedem der drei Eventhandler die jeweils passende Methode auf. Nun sollten die beiden Paddles gesteuert werden können.
Verfeinern Sie das Spiel weiter, indem Sie einen Startbutton integrieren, der das Spiel startet, sobald Sie auf
(Falls alles funktioniert, sollten Sie die <code>console.log</code>-Befehle in den Eventhandler auskommentieren oder löschen.)
ihn klicken. Sie brauchen dazu ein Bild (<code>imgStart</code> das Sie in <code>config.json</code>
konfigurieren und in <code>app.js</code> laden müssen. Das Bild eines kreisrunden Buttons finden Sie im
Bilder-Ordner: <code>start-200.png</code>.


Sie müssen in der Datei <code>config.json</code>
außerdem das Model und die View des Startbuttons geeignet konfigurieren und die zugehörigen
Model- und Viewobjekte im App-Modul definieren und initialisieren. Da das Bild kreisrund ist,
sollten Sie die Kalssen <code>ModelCircle</code> und </code>ViewCircleSprite</code> verwenden.
Vergessen Sie nicht, das Model in das Objekt <code>c_models</code> einzufügen. Außerdem
müssen Sie die View in das Array von <code>initRenderer</code> einfügen. 
Im Update-Modul müssen Sie dafür sorgen, dass die Update-Funktion des Startbuttons regelmäßig aufgerufen wird.


====Behandlung von Ball-Events durch die Logik====
Das bisherige Vorgehen ist etwas problematisch, da Sie die Game Loop bei Spielende nicht mehr anhalten
Jetzt fehlt noch die Reaktion der Logik auf das Verschwinden des Balls.
können. Der Grund ist, dass auch der Button mit Hilfe der Loop aktualisiert und gerendert wird. Es wäre
zwar kein Problem darauf zu verzichten, aber es ist gar nicht schlecht, die Game Loop dauerhaft laufen
zu lassen, um auch nach Spielende bestimmte Elemente auf der Bühne animieren zu können.


Fügen Sie in die Datei <code>control/control.js</code> zwei weitere „Konstanten“ ein:
Das heißt aber, dass Sie die Klasse <code>ModelCircle</code> un drei Methoden erweitern müssen,
mit denen Sie den Ball jederzeit in der Mitte des Spielfelds platzieren, mit zufälliger Flugrichtung starten
und  auch wieder anhalten können.


<source lang="javascript">
<source lang="javascript">
control.C_EVENT_BALL_LEAVES_LEFT  = 'EventBallLeavesLeft';
moveTo({x=0, y=0} = concretize({config: this.config}))
control.C_EVENT_BALL_LEAVES_RIGHT = 'EventBallLeavesRight';
{ this.x = x; this.y = y; }
</source>
 
 
start()
Sorgen Sie dann dafür, dass das Modul <code>collisionStageBall</code> das jeweils passende Ereignis meldet,
{ const {vx=0, vy=0} = concretize({config: this.config});
wenn der Ball die Bühne auf der linken Seite verlässt (der rechte Rand des Balls ist kleiner als der linke Rand der Bühne)
   this.vx = vx; this.vy = vy;
oder auf der rechten  (der linke Rand des Balls ist größer als der rechte Rand der Bühne). Detailinformationen brauchen Sie
in dem Event-Objekt nicht zu speichern, das es ja nur einen Ball sowie einen linken und einen rechten Bühnenrand gibt.
Die Logik erkennt schon am Ereignistyp, was passiert ist. '''Achtung''': Vergessen sie nicht, das Modul <code>control.js</code> zu importieren.
 
Zum Testen der Events können Sie wie schon im Fall der Tastatur-Ereignisse zwei Event-Listener direkt im Modul <code>control.js</code>
definieren und geeignete <code>console.log</code>-Befehle einfügen. Sobald dies funktioniert verschieben Sie die beiden Befehle
wieder in den Rumpf der Init-Funktion des Logik-Moduls.
 
Fügen Sie '''in''' den Rumpf der Logikfunktion folgende Hilfsfunktion ein (da diese Funktion '''innerhalb''' des Rumpfes von <code>init</code> auf
<code>p_remove</code> und <code>p_add</code> zugreifen kann):
<source lang="javascript">
function f_new_ball()
{ p_remove(p_models.ball);
   p_add(concretize(c_init_ball), 'ball');
}
}
</source>
 
stop()
{ this.vx = 0; this.vy = 0; }
</source>  


Vergessen Sie nicht die Funktion <code>concretize</code> aus dem Modul <code>wk/Util</code> zu importieren.
Jetzt müssen Sie in der Logik eine neue Startfunktion definieren, außerdem müssen Sie <code>ballLoss</code>
 
so umschreiben, dass das Spiel nicht mehr mittels einens Stopps der Game Loop angehalten wird:
Diese Funktion löscht das alte Ball-Objekt mittels der Remove-Funktion, die vom Game-Modul bereit gestellt wird.
Und es erzeugt ein neues Objekt mit demselben Model-Namen <code>ball</code>, den das gelöschte Ball-Objekt hatte.
Wie üblich muss bei der Erzeugung eine Spiel-Model-Objektes ein Konfigurationsobjekt übergeben werden.
Fügen Sie eine derartiges Objekt zunächst direkt (im Anschluss an die Import-Befehle) in das Logik-Modul ein:


<source lang="javascript">
<source lang="javascript">
const
import wait from '/wk/util/wait';
  c_init_ball
    = { "view": "ball", "class": "MovableCircle", "isMovable": true,
        "r":    15,
        "x":    300,
        "y":    200,
        "vx":  { "@min": 150, "@max": 300, "@positive": 0.5 },
        "vy":  { "@min": 150, "@max": 300, "@positive": 0.5 }
      };
</source>


Nun können Sie jeweils einen Aufruf dieser Hilfsmethode in die beiden Event-Listener für die Ereignisse <code>C_EVENT_BALL_LEAVES_LEFT</code>
let
und <code>C_EVENT_BALL_LEAVES_RIGHT</code> einfügen.
  v_game_loop, v_start_button, v_ball, v_paddles, v_scores, v_win_score;


Ab sofort sollte sofort, wenn der Ball die Bühne verlässt, ein neuer Ball in der Mitte der Bühne
function initLogic(p_game_loop,
erzeugt werden und sich in zufällige Richtung los bewegen. (Wenn Sie Ihre Bühne nicht 600 mal 400 Pixel groß gewählt haben,  
                  { startButton: p_start_button,
müssen Sie die x- und die y-Position des Balls anpassen, damit er von der Mitte der Bühne aus startet.)
                    ball:        p_ball,
                    paddles:    p_paddles,
                    scores:      p_scores
                  },
                  { winScore: p_win_score = 10
                  } = {}
                  )
{ v_game_loop    = p_game_loop;


''Anmerkung'': Man könnte auch noch ein Timer-Objekt ([https://developer.mozilla.org/de/docs/Web/API/WindowTimers/setTimeout WindowTimers.setTimeout()]) erstellen,
  v_start_button = p_start_button;
das das neue Ball-Objekt erst nach einer gewissen Zeitspanne erzeugt ({{zB}} 500 Millisekunden). Dazu müsste allerdings das Logik-Objekt
  v_ball        = p_ball;
auf die Game-Loop zugreifen können, um das Spiel anzuhalten., während kein Ball im Spiel ist. Das ist etwas für Pong02.
  v_paddles      = p_paddles;
  v_scores      = p_scores;
 
  v_win_score    = p_win_score;
 
  v_ball.moveTo();        // move the ball to its start point
  v_ball.stop();
  v_start_button.reset(); // make the start button visible
  v_game_loop.start();
}


Eine Unsauberkeit sollten Sie noch beheben. Die Definition der Konstanten <code>c_init_ball</code> im Modul <code>logic</code>
async function startGame()
widerspricht dem Prinzip „[[Programmierprinzipien|Schreibe konfigurierbaren Code]]“. Wenn Sie beispielsweise in der Konfigurationsdatei <code>config01.json</code>
{ v_scores[0].score = 0;
die Grüße der Bühne ändern, müssen Sie zurzeit zusätzlich in der Datei <code>logic/logic.js</code> die Position des Balls im Objekt
  v_scores[1].score = 0;
<code>c_init_ball</code> ändern, damit er weiterhin von der Mitte aus startet. Die Erfahrung zeigt, dass das regelmäßig vergessen wird. Außerdem
  v_start_button.x = -2*v_start_button.r; // move the start button outside
findet man, wenn die Programmierung schon etwas zurück liegt, die Datei, in der die Änderung erfolgen muss, {{iAllg}} nur schwer.
                                          // the stage to make it invisible
  await wait(1000);
 
  v_ball.start();                        // start the game
}


Das Objekt, das derzeit in der Konstanten <code>c_init_ball</code> gespeichert wird, sollte unbedingt in der Datei <code>config01.json</code> gespeichert
function ballLoss(p_player)
werden und nicht in der Datei <code>logic.js</code>. Dabei gibt es allerdings zwie Schwierigkeiten:
{ // the score of the other player is raised
 
  const c_player = p_player === 'left' ? 1 : 0;
# Das Ball-Objekt soll nicht zum Startzeitpunkt der Anwendung erstellt werden, sondern vom Logik-Modul, wenn ein neues Spiel gestartet wird. Daher kann das Initialisierungsobjekt nicht im Objekt <code>model</code> innerhalb der Konfigurationsdatei werden. Alle Model-Objekte, deren Konfiurationsinformationen dort abgelegt sind, werden bereits zum Startzeitpunkt erstellt.
  v_scores[c_player].score++;
# Jedesmal, wenn ein neues Ball-Objekt vom Logik-Modul erzeugt wird, soll es eine zufällige Geschwindigkeit haben. Das erfolgt mit Spezifikationen der Art  <code>"vx":   {"@min": 150, "@max": 300, "@positive": 0.5}</code>. Die Funktion <code>concretize</code> aus dem Modul <code>wk/Util</code> ersetzt das Objekt <code>{"@min": 150, "@max": 300, "@positive": 0.5}</code> durch einen Wert zwischen <code>150</code> und <code>300</code> und negiert diesen Wert in 50% der Fälle. Allerdings wird die Funktion <code>concretize</code>  schon bei Programmstart auf die gesamte Konfigurationsdatei angewendet, so dass ein Element des Konfigurationsobjekts, das irgendeinem Modul übergeben wird, nie ein derartiges Konstrukt enthält. Es enthält stattdessen einen zufällig gewählten Wert aus dem angegebenen Bereich, der sich aber nicht mehr ändert.
 
 
   if (v_scores[c_player].score === v_win_score) // game over
Beide Probleme können mit der aktuellen Implementierungen des Game-Moduls und der Funktion <code>concretize</code>.
  { v_ball.moveTo();        // Put the ball in the middle of the stage
 
    v_ball.stop();          // so that no collision with the outside of
ad 1.) Objekte, die im Konfigurationsobjekt <code>init</code> eingefügt werden, werden dem Logik-Modul im Parameter <code>p_init</code> zur Verfügung gestellt (allerdings in konkretisierter Form).
                            // the stage is detected.
ad 2.) Die Funktion <code>concretize</code> ersetzt einen doppelten Klammeraffen durch einen einfachen Klammeraffen. Das heißt, die Konkretisierung von
    v_start_button.reset(); // Make the start button visible again.
 
<source lang="javascript">
{"@min": 150, "@max": 300, "@positive": 0.5}
</source>
 
liefert eine Zahl zwischen 150 und 300 oder zwischen -300 und -150 als Ergebnis,
wohingegen die Konkretisierung von
 
<source lang="javascript">
{"@@min": 150, "@@max": 300, "@@positive": 0.5}
</source>
 
das Objekt <code>{"@min": 150, "@max": 300, "@positive": 0.5}</code> als Ergebnis liefert.
 
Damit lässt sich das oben beschriebene Problem beheben. Fügen Sie in die Datei <code>config02</code>
folgendes Objekt vor dem Objekt <code>model</code> ein:
 
<source lang="javascript">
"init":
{
  "ball":
  {
    "view": "ball", "class": "MovableCircle", "isMovable": true,
    "r":      15,
    "x":    300,
    "y":    200,
    "vx":    { "@@min": 150, "@@max": 300, "@@positive": 0.5 },
    "vy":    { "@@min": 150, "@@max": 300, "@@positive": 0.5 }
   }
   }
},
  else // the next round starts
</source>
  { v_ball.reset(); }
}


Fügen Sie in die Funktion <code>init</code> des Moduls <code>logic</code> dem Befehl <code>console.log(p_init);</code> und sehen Sie sich das Ergebnis an.
export {initLogic, startGame, ballLoss};
</source>  


Nun können sie die Konstante <code>c_init_ball</code> aus dem Logik-Modul löschen und in der Funktion <code>f_new_ball</code> den Aufruf
Beachten Sie, dass die Startfunktion asynchron definiert wurde, um nach den Klicken
<code>concretize(c_init_ball)</code> durch <code>concretize(p_init.ball)</code> ersetzen.
des Startbuttons noch mittesl <code>await wait(1000)</code> etwas zu warten zu können,
bis der Spieler die Maus aus dem Spielfeld bewegt hat.


===Textausgabe===
Als letztes benötigen Sie einen Controller für den Startbutton, der die zuvor in der Logik definierte
[[Datei:WK Pong01 ClassModel01 text.png|gerahmt|ohne|Textmodule]]
Startfunktion aufruft, sobald der Spieler den Startbutton betätigt. Wenn man die PixiJS-Events
 
verwendet, die bei einem Klick auf ein grafisches Element verschickt werden, ist es ganz einfach
Jetzt fehlt nur noch die Ausgabe des Punktestands. Für die Verwaltung des Punktestands brauchen Sie kein Punkte-Model. Es reichen
einen derartigen Controller zu implementieren:
zwei Variablen <code>v_score_player1</code> und <code>v_score_player2</code>, die jedes Mal, wenn der Ball 
die Bühne verlässt entsprechende der Seite, auf der der Ball die Bühne verlassen hat, um Eins erhöht wird.
 
Für die Ausgabe des Textes benötigen Sie die beiden Module <code>model/ImmovableText.js</code> und  <code>view/ViewText.js</code>.
Im ersten Modul die Klasse <code>ImmovableText</code> definiert, die alle Properties der Klasse <code>Immovable</code>
erbt und das Attribut <code>text</code> zur Propertyliste hinzufügt. Da wird mit dem Befehl <code>super(mixin ({text: ''}, p_config));</code>
im Konstruktor erledigt. (Zur Erinnerung: Den Befehl <code>mixin</code> finden Sie im Modul <code>wk/util</code>.)
 
Die Klasse <code>ViewText</code> wird analog zur Klasse <code>ViewGraphics</code> (<code>wk/view/ViewGraphics</code>)
erstellt. Nur wird diesmal kein Grafik-Objekt erstellt (<code>new p_pixi.Graphics()</code>), sondern ein Text-Objekt
(<code>new p_pixi.Text(p_model.text, p_config)</code>), dem der aktuelle Text aus dem Model-Objekt sowie das Konfigurationsobjekt
mit beliebigen [http://pixijs.download/dev/docs/PIXI.TextStyle.html Style-Attributen] als Argumente übergeben wird.
 
Wichtig ist noch, dass Sie eine Update-Funktion einfügen, damit die View stets den aktuell im Modell gespeicherten Text anzeigt:


<source lang="javascript">
<source lang="javascript">
update()
import {startGame} from '../logic/logic';
{ super.update();
  this.sprite.text = this.model.text;
}
</source>


Wie bereits ziemlich zu Beginn des Dokument erwähnt wurde, wird in Pong02 die Update-Funktion durch einen Event-Handling-Mechanismus ersetzt:
function controlStart(p_start)
Jedes Mal, wenn sich im Model etwas ändert, werden alle zugehörigen View-Objekte (es kann durchaus mehr als ein View-Objekt für ein Model-Objekt
{ p_start.addEventListener('pointerdown', () => startGame()); }
geben) über die Änderung informiert. In diesem Moment aktualisieren die View die zugehörige Darstellung. Damit ist die Update-Methode
im View-Objekt überflüssig. Diese aktualisiert derzeit '''jedes Mal''', bevor ein View-Objekt auf die Bühne gezeichnet wird, den darzustellenden Inhalt,
auch wenn sich das zugehörige Model gar nicht verändert hat und somit an der Darstellung gar nichts geändert werden muss. Der
Event-Handling-Mechanismus sorgt dafür, dass View-Updates nur dann durchgeführt werden, wenn sich im zugehörigen Model auch etwas geändert hat.


Nun ist es an der Zeit, die beiden View-Objekte in die Anwendung einzubinden.
export {controlStart};
Dabei gehen Sie wie üblich vor:
</source>


* Importieren der Module code>ImmovableText</code> und  <code>ViewText</code> in das Game-Modul. Außerdem muss die JSON-Map muss geeignet werden.
Jetzt brauchen Sie die Funktion <code>controlStart</code> nur noch in das App-Modul zu importieren
* Konfiguration zweier Text-Objekte samt zugehöriger View (mit [http://pixijs.download/dev/docs/PIXI.TextStyle.html PIXI.TextStyle]-Attributen!). Positionieren Sie das eine Text-Objekt in der linken Spielfeldhälfte und das andere in der rechten. Denken Sie daran, das Anchoring von Texten funktioniert noch nicht.
und für die View des Startbuttons aufrufen, sobald diese View definiert wurde.
* Ändern des Textattribut-Wertes im Model-Objekt, wann immer ein Spieler einen Punkt erhält.


===Abschlussarbeiten===
Wenn alles funktioniert, können Sie nun mehrere Runden Pong spielen:
<br/>https://glossar.hs-augsburg.de/beispiel/tutorium/2018/pong/WK_Pong01/web/index05.html


Haben Sie eigentlich jedes Mal daran gedacht, in Dateien, die Sie erstellt haben,
===Aufgabe 6===
Ihren Namen ins Kommentarfeld <code>@author</code> zu schreiben?
(Musterlösung: [https://gitlab.multimedia.hs-augsburg.de/kowa/WK_Pong01.git Gitlab: <code>WK_Pong01</code>, <code>app06</code>], [https://glossar.hs-augsburg.de/beispiel/tutorium/2018/pong/WK_Pong01/web/index06.html index06.html])


Es fehlt auch noch die Implementierung des Use Case: „Spielende, sobald einer der Spieler 10 Punkte erreicht hat“. Auch dafür muss das Logik-Modul
Erweitern Sie das Spiel so, dass bei Spielende der Name des Siegers
auf die Game-Loop zugreifen können. Daher ist auch dies ein Punkt, der erst in Pong02 behandelt wird.  
(„linker Spieler“ oder „rechter Spieler“) in einem Textfeld ausgegeben wird, bevor der Startbutton
-->
wieder eingeblendet wird:
<br/>https://glossar.hs-augsburg.de/beispiel/tutorium/2018/pong/WK_Pong01/web/index06.html


==Quellen==
==Quellen==
<references/>
<references/>
<ol>
<ol>
<li value="4"> {{Quelle|Kowarschick, W.: Multimedia-Programmierung}}</li>
<li value="1"> {{Quelle|Kowarschick, W.: Multimedia-Programmierung}}</li>
</ol>
</ol>

Aktuelle Version vom 1. März 2023, 14:49 Uhr

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/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 Projekts 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

Konfiguration des Balls

Platzieren Sie den Ball zu Beginn des Spiels in der Mitte der Bühne, indem Sie die Konfigurationsdatei entsprechend abändern. Sie können auch noch die Ballgröße anpassen, indem Sie ein beispielsweise einen Radius von nur 12 Pixeln wählen:

"ball":
{ "r":   15,
  "x":   15,
  "y":   15,
  "vx": 200,
  "vy": 150
}

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_models_paddle = 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_models_paddle =
  [ new ModelRectangle(c_config_models_paddle[0]),
    new ModelRectangle(c_config_models_paddle[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_models_paddle
           });
  • 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_models_paddle[0],c_config_view_paddle),
  new ViewRectangleGraphics
      (c_pixi_app, c_models_paddle[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 Arrays 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 gewünschten Wert als Ergebnis liefert.
  • Definieren Sie die Klasse ModelPaddle als Subklasse der Klasse ModelRectangle. Der Konstruktor constructor(p_config) übergibt das Konfigurationsobjekt 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 Klasse ModelRectangle auf die im Konfigurationsobjekt übergebenen Werte zurücksetzen. Die Geschwindigkeit und die Beschleunigung 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 der 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_models_paddle[1].up();

Damit sollte sich – sofern Sie Ihr Array mit den beiden Schläger-Models c_models_paddle 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_models_paddle[0], c_config_control_paddles[0]);
new ControlPaddle(c_models_paddle[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":    580,
    "y":     20,
    "score":  0
  }
]

"view": 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 Ende der Konfigurationsdatei, in der definiert 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}
  • Damit das Spiel wieder funktioniert, müssen Sie die Klasse ModelCircle so erweitern, dass die Resetfunktion bei jedem Aufruf die Funktion concretize auf das Konfigurationsobjekt anwendet, bevor sie die Attribute des Ballobjektes auf die Initialwerte zurücksetzt. Improtieren Sie zunächst die Funktion /wk/util/concretize in das Modul. Anschließend müssen Sie den Konstruktor und die Resetfunktion so umschreiben, dass die Zerlegung des Konfigurationsobjektes (Destructuring) nicht mehr im Konstruktor, sondern erst in der Resetfunktion durchgeführt wird, nachdem concretize darauf agewendet wurde (dabei wird auf die Attribute this.rConfig, this.xConfig etc. verzichtet; stattdessen wird das ganze Konfigurationsobjekt in this.config dauerhaft gespeichert):
class ModelCircle
constructor(p_config = {})
{ this.config = p_config;
  this.reset();
}
  
reset()
{ const {r=0, x=0, y=0, vx=0, vy=0} = concretize({config: this.config});
  this.r  = r;
  this.x  = x;  this.y  = y;
  this.vx = vx; this.vy = vy;
}
...
  • Jetzt sollte das Spiel wieder funktionieren, mit dem Unterschied, dass der Ball bei jedem Aufruf von der Mitte aus in eine zufällige Richtung losfliegt.
  • Wenn Sie möchten, können und sollten Sie (aus Symmetriegründen) auch die Klassen ModelRectangle und ModelPaddle so umschreiben, dass das Destructuring erst in der Resetfunktion stattfindet.
  • Nun müssen Sie die beiden Klassen ModelScore und ViewText zur Verwaltunge der Score-Textfelder erstellen (und im jeweiligen Modul auch exportieren!).
    • Der Konstruktor von ModelScore hat einen Parameter p_config in dem ihm ein Konfigurationsobjekt übergeben wird, das zuvor in der Konfigurationsdatei definiert wurde (beispielsweise { "x": 20, "y": 20, "score": 0}). Diese Objekt wird wie auch schon in der Klasse ModelCircle im Attribut this.config gespeichert und in der Resetfunktion zum Initialisieren der drei Attribute this.score, this.x und this.y verwendet. Auf den Einsatz der Funktion ycodecretize kann hier verzichtet werden. (Es würde aber auch keine Probleme verursachen, wenn sie dennoch aufgerufen werden würde.)
    • Neben dem Konstruktor, und der Resetfunktion gibt es noch eine Gettermethode text, mittels der die Renderfunktion auf den Stringwert des Scores zugreifen kann. (Im Attribut this.code wird ein Zahlwert gespeichert, damit dieser mittels des ++-Operators einfach hochgezählt werden kann.
get text()
{ return this.score.toString(); }
  • Die Klasse ViewText ist im Prinzip wie die Klasse ViewSpriteCircle aufgebaut. Die wesentlichen Unterschiede sind:
    • Es wird die PixiJS-Klasse Text nstelle der Klasse Sprite importiert.
    • Das Konfigurationsobjekt, das dem Konstruktor übergeben wird, enthält kein Attribut image, sondern die Attribute style und anchor.
    • Im Model-Objekt wird ein Objekt übergeben, dass in einem Attribut text den Text bereitstellt, der visualisiertwerden soll. Die Objekte der Klasse ModelScore haben so ein Attribut.
    • Das Sprite-Objekt wird mittels const c_sprite = new Text(p_model.text, p_style) erstellt, wobei p_model der Parameter ist, indem dem Konstruktor das Score-Model übergeben wurde, und p_style das gewünschte Styleobjekt aus der Konfigurationsdatei enthält. In diesem Objekt dürfen alle Styleattribute verwendet werden, die PixiJS unterstützt: {http://pixijs.download/dev/docs/PIXI.TextStyle.html PIXI.TextStyle]. Anschließend muss noch der Anchor des Textobjektes korrekt gesetzt werden: c_sprite.anchor = p_anchor;.
    • Der Rest des Konstruktors stimmt mit dem Rest des Konstruktors der Klasse ViewSpriteCircle überein.
    • Die Renderfunktion muss nicht nur das Text-Objekt korrekt platzieren, sondern auch noch den gewünschten Text darstellen. Fügen Sie daher den folgenden Befehl ein:
this.sprite.text = this.model.text;
  • Erzeugen und initialisieren Sie die beiden Score-Textfelder wie üblich im App-Modul:
    • ModelScore und ViewText importieren,
    • c_config_models_score und c_config_view_text wie üblich definieren,
    • c_models_score analog zu c_models_paddle definieren (mit ModelScore als Konstruktor),
    • c_views_score analog zu c_views_paddle definieren (mit ViewText als Konstruktor).
    • Fügen Sie das Array c_views_score analog zu c_view_paddle in das Array ein, das der Funktion initRenderer als Argument übergeben wird, damit künftig auch die Textfelder gerendert werden.
    • Sie haben nun soviele Models definiert, dass es sich rentiert, eine Konstante c_models zu definieren, die alle Models enthält und diese Konstante sowohl an fie Funktion initUpdater zu übergeben (initUpdater(c_models);) als auch später an die Logic (sobald diese definiert ist):
c_models =
  { stage:   c_model_stage,
    ball:    c_model_ball,
    paddles: c_models_paddle,
    scores:  c_models_score
  }
  • Als nächstes müssen Sie die Logik in der Datei logic/logic.js implementieren. Diese stellt wie üblich eine Funktion initLogic bereit, in der ihr alle wesentlichen Objekte übergeben werden, die Sie kennen muss, um das Spiel zu verwalten. Das sind die Gam Loop (da sie das Spiel starten und stoppen können sollte), die Models sowie das Konfigurationsobjekt logic aus der Konfigurationsdatei. Wie üblich muss diese Funktion die ihr übergebenen Werte in (dateiinternen) Variablen speichern. Anschließend sollte sie das Spiel starten:
function initLogic(p_game_loop,
                   {ball: p_ball, paddles: p_paddles, scores: p_scores},
                   {winScore: p_win_score = 10} = {}
                  )
{ v_game_loop = p_game_loop;
  v_ball      = p_ball;
  v_paddles   = p_paddles;
  v_scores    = p_scores;
  v_win_score = p_win_score;

  // start the game
  v_game_loop.start();
}
  • Darüber hinaus muss die Logik eine Funktion implementieren, die die Kollissionserkennung aufrufen kann, wenn ein Spieler einen Ball passieren lässt. Diese muss die Punkte des Gegners erhöhen, die Game Lopp stoppen, wenn das Spiel vorbei ist oder eine neue Runde starten, wenn das Spiel noch nicht vorbei ist.
function ballLoss(p_player)
{ // the score of the other player is raised
  const c_player = p_player === 'left' ? 1 : 0;
  v_scores[c_player].score++;
  
  if (v_scores[c_player].score === v_win_score) // game over
  { v_game_loop.stop();
    v_ball.reset();
  }
  else // the next round starts
  { v_ball.reset(); }
}
  • Beide Funktionen müssen exportiert werden.
  • Als nächstes müssen Sie dafür sorgen, dass die Kollisionsfunktion collisionCircleStage den Ball nicht mehr an den Wänden hinter den Schlägern abprallen lässt. Anstatt dessen muss der Ball die Bühne verlassen und die Logik muss informiert werden, welcher Spieler (= Schläger) den Ball verlohren hat. Improtieren Sie dazu in diesem Modul die zuvor definierte Funktion ballLoss aus der Logik und ersetzen Sie die bisherigen Kollsisionsabfragen für die linke und die rechte Wand durch folgenden Code:
if (p_circle.right < p_stage.left)
  { ballLoss('left'); }
  if (p_circle.left > p_stage.right)
  { ballLoss('right'); }
  • Integrieren Sie die Logik nun auch noch ins Modul app:
    • Importieren Sie die Funktion initLogic.
    • Definieren Sie c_config_logic wie üblich.
    • Ersetzen Sie zum Schluss den Start der Game Loop am Ende der Initfunktion durch die Initialisierung der Logik:
initLogic( new GameLoop({update: update, render: render}),
             c_models,
             c_config_logic
           );

Wenn jetzt 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)

Verfeinern Sie das Spiel weiter, indem Sie einen Startbutton integrieren, der das Spiel startet, sobald Sie auf ihn klicken. Sie brauchen dazu ein Bild (imgStart das Sie in config.json konfigurieren und in app.js laden müssen. Das Bild eines kreisrunden Buttons finden Sie im Bilder-Ordner: start-200.png.

Sie müssen in der Datei config.json außerdem das Model und die View des Startbuttons geeignet konfigurieren und die zugehörigen Model- und Viewobjekte im App-Modul definieren und initialisieren. Da das Bild kreisrund ist, sollten Sie die Kalssen ModelCircle und ViewCircleSprite verwenden. Vergessen Sie nicht, das Model in das Objekt c_models einzufügen. Außerdem müssen Sie die View in das Array von initRenderer einfügen. Im Update-Modul müssen Sie dafür sorgen, dass die Update-Funktion des Startbuttons regelmäßig aufgerufen wird.

Das bisherige Vorgehen ist etwas problematisch, da Sie die Game Loop bei Spielende nicht mehr anhalten können. Der Grund ist, dass auch der Button mit Hilfe der Loop aktualisiert und gerendert wird. Es wäre zwar kein Problem darauf zu verzichten, aber es ist gar nicht schlecht, die Game Loop dauerhaft laufen zu lassen, um auch nach Spielende bestimmte Elemente auf der Bühne animieren zu können.

Das heißt aber, dass Sie die Klasse ModelCircle un drei Methoden erweitern müssen, mit denen Sie den Ball jederzeit in der Mitte des Spielfelds platzieren, mit zufälliger Flugrichtung starten und auch wieder anhalten können.

moveTo({x=0, y=0} = concretize({config: this.config}))
{ this.x = x; this.y = y; }
  
start()
{ const {vx=0, vy=0} = concretize({config: this.config});
  this.vx = vx; this.vy = vy;
}
  
stop()
{ this.vx = 0; this.vy = 0; }

Jetzt müssen Sie in der Logik eine neue Startfunktion definieren, außerdem müssen Sie ballLoss so umschreiben, dass das Spiel nicht mehr mittels einens Stopps der Game Loop angehalten wird:

import wait from '/wk/util/wait';

let
  v_game_loop, v_start_button, v_ball, v_paddles, v_scores, v_win_score;

function initLogic(p_game_loop,
                   { startButton: p_start_button,
                     ball:        p_ball,
                     paddles:     p_paddles,
                     scores:      p_scores
                   },
                   { winScore: p_win_score = 10
                   } = {}
                  )
{ v_game_loop    = p_game_loop;

  v_start_button = p_start_button;
  v_ball         = p_ball;
  v_paddles      = p_paddles;
  v_scores       = p_scores;
  
  v_win_score    = p_win_score;
  
  v_ball.moveTo();        // move the ball to its start point
  v_ball.stop();
  v_start_button.reset(); // make the start button visible
  v_game_loop.start();
}

async function startGame()
{ v_scores[0].score = 0;
  v_scores[1].score = 0;
  v_start_button.x = -2*v_start_button.r; // move the start button outside
                                          // the stage to make it invisible
  await wait(1000);
  
  v_ball.start();                        // start the game
}

function ballLoss(p_player)
{ // the score of the other player is raised
  const c_player = p_player === 'left' ? 1 : 0;
  v_scores[c_player].score++;
  
  if (v_scores[c_player].score === v_win_score) // game over
  { v_ball.moveTo();        // Put the ball in the middle of the stage
    v_ball.stop();          // so that no collision with the outside of
                            // the stage is detected.
    v_start_button.reset(); // Make the start button visible again.
  }
  else // the next round starts
  { v_ball.reset(); }
}

export {initLogic, startGame, ballLoss};

Beachten Sie, dass die Startfunktion asynchron definiert wurde, um nach den Klicken des Startbuttons noch mittesl await wait(1000) etwas zu warten zu können, bis der Spieler die Maus aus dem Spielfeld bewegt hat.

Als letztes benötigen Sie einen Controller für den Startbutton, der die zuvor in der Logik definierte Startfunktion aufruft, sobald der Spieler den Startbutton betätigt. Wenn man die PixiJS-Events verwendet, die bei einem Klick auf ein grafisches Element verschickt werden, ist es ganz einfach einen derartigen Controller zu implementieren:

import {startGame} from '../logic/logic';

function controlStart(p_start)
{ p_start.addEventListener('pointerdown', () => startGame()); }

export {controlStart};

Jetzt brauchen Sie die Funktion controlStart nur noch in das App-Modul zu importieren und für die View des Startbuttons aufrufen, sobald diese View definiert wurde.

Wenn alles funktioniert, können Sie nun mehrere Runden Pong spielen:
https://glossar.hs-augsburg.de/beispiel/tutorium/2018/pong/WK_Pong01/web/index05.html

Aufgabe 6

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

Erweitern Sie das Spiel so, dass bei Spielende der Name des Siegers („linker Spieler“ oder „rechter Spieler“) in einem Textfeld ausgegeben wird, bevor der Startbutton wieder eingeblendet wird:
https://glossar.hs-augsburg.de/beispiel/tutorium/2018/pong/WK_Pong01/web/index06.html

Quellen

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