MMProg: Praktikum: WiSe 2018/19: Ball03
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) |
Inhalt | EcmaScript01 | EcmaScript02 | EcmaScript03 | Ball 01| Ball 02 | Ball 03 | Pong 01
Musterlösung: Web-Auftritt (Git-Repository noch nicht online)
Ziel
Ziel dieser Praktikumsaufgabe ist es, Lösungen des zweiten Teils des Tutoriums zu modularisieren. Die Modularisierung hat mehrere Vorteile:
- Das Prinzip „Don't repeat yourself“ (DRY) wird unterstützt. Um dies zu erreichen, muss sichergestellt werden, dass viele Module in diversen Projekten wiederverwendet werden können.[1][2]
- Mehrere Programmierer können gleichzeitig an einem Projekt arbeiten. Hier muss sichergestellt sein, dass möglichst saubere Schnittstellen definiert werden, damit ein Programmierer nicht ständig an die Änderungen eines anderen Programmierers anpassen muss. (Die Definition von Schnittstellen ist eine der Kernaufgaben von Informatikern. Dabei handelt es sich um einen kreativen Prozess. Der Programmierer hingegen braucht „lediglich“ die Spezifikation umzusetzen. Das ist i. Allg. deutlich weniger kreativ.)
- Jede Änderung an einem vorhandenen Code kann Fehler zur Fogle haben. Daher ist es von Vorteil, wenn ausgetestete, wiederverwendbare Module bestehen. Bei der Fehlersuche befindet sich der Code i. Allg. nicht in einem dieser Module, sondern im neu erstellten Code. Das vereinfacht die Fehlersuche deutlich.
Vorbereitung
Importieren Sie das leere Git-Projekt Ball03 in WebStorm.
Laden Sie anschließend mittels npm i
alle benötigten Node.js-Module in das Projekt.
Sie können Ihr Projekt zur Übung auch in Ihrem Git-Repository speichern. Das ist aber nicht so wichtig. Falls Sie das machen möchten, müssen Sie es zuvor von meinem (schreibgeschützten) Repository lösen:
git remote remove origin
git remote add origin https://gitlab.multimedia.hs-augsburg.de:8888/BENUTZER/Ball03.git
Aufgaben
In Ihrem Projekt finden Sie drei Web-Anwendungen: src/index01.html
, src/index02.html
und src/index03.html
.
Allerdings existiert derzeit nur der JavaScript-Ordner src/js/app01
für die Web-App index01.html
.
Die fehlenden JavaScript-Ordner src/js/app02
und src/js/app03
werden im Laufe des Tutoriums erstelltt und gefüllt.
Im Ordner src/js/app01
befinden sich derzeit zwei Dateien: app.js
und game.js
. Diese Dateien enthalten eine App, bei der sich ein Ball schräg über die Bühne bewegt. Diese App ist eine Mischung der Musterlösungen von Teil 2 des Ball-Tutoriums: Bei der View des Balls wurde die Graphics
-Version aus der ersten Teilaufgabe dieser Tutoriumsaufgabe genommen, die Update-Funktionalität und die Kollisionserkennung
entstammt dagegen dem vierten Teil.
Beachten Sie, dass sich die Ordnerstruktur gegenüber Teil 2 des Tutoriums geändert hat.
Im Ordner src/js
gibt es für jede Web-App einen Ordner mit Namen appXY
,
in dem eine Datei app.js
enthalten sein muss, die die App initialisiert. Es können
im selben Ordner beliebig viele weitere Dateien und Unterordner enthalten sein, die von app.js
direkt oder indirekt importiert werden. Die Umstrukturierung ist sinnvoll, da ab sofort eine Web-App aus deutlich mehr als drei Dateien bestehen wird.
Zu jedem Web-App-Ordner src/js/appXY
muss es eine zugehörige Datei
src/indexXY.html
geben.
Anmerkung: Die Datei webpack.config.js
wurde speziell für diese Struktur entwickelt.
Die Datei webpack.config.js
von Teil 02 ist nicht kompatibel mit der
webpack-Konfigurationsdatei von Teil 3.
Aufgabe 1: Analyse von app01
Es gibt in WK_Ball03
eine app01
mit zwei Modulen app
und game
, die in zwei Dateien app.js
und game.js
implementiert wurden.
Sehen Sie sich zunächst diese App an und analysieren Sie, wie diese funktioniert. Beachten Sie insbesondere Folgendes:
- Es wird ein Ball-Objekt erzeugt, mit Attributen „Radius“, „Position“ und „Geschwindigkeit“.
- Dieser Ball wird durch einen einfarbigen Kreis mit Rand visualisiert.
- Der Ball bewegt sich gemäß seinen Initialparametern schräg über die Bühne.
- Bei einer Kollission mit dem Bühnenrand ändert er seine Bewegungsrichtung.
Die gesamte beschriebene Spiellogik ist in einer Datei enthalten: game.js
. So eine Datei bezeichne ich als Moloch.
Das Prinzip „Implementiere keinen Moloch“ nennt man fachsprachlich „Modularisierung“.
Eigentlich ist das Programm gar nicht so molochartig, wie es zunächst scheint. Es kommen einige Module zum Einsatz:
index01.html
: Eine HTML-Seite zum Starten der eigentlichen Web-Anwendung, sobald sie vom Browser geladen wird.head.css
: Eine CSS-Datei, die das Layout der HTML-Datei festlegt (insbesondere die Hintergrundfarbe), solange die App geladen wird. Diese DAtei wird direkt vonindex01.html
eingebunden. Mittels webpack wird der Inhalt dieser Datei komprimiert und direkt in die zugehörige HTML-Datei injiziert.app.css
: Eine CSS-Datei, die das Layout der Web-App festlegt. Diese Datei wird vonapp.js
importiert. Mittels webpack wird dafür gesorgt, dass (die transformierte Version von) der Inhalt der CSS-Datei inindex01.html
vonapp.js
injiziert wird, sobald diese Datei vollständig geladen wurde.GameLoop
: Eine Game-Loop-Klasse, die Ihnen im Rahmen des Praktikums zur Verfügung gestellt wird. Diese Modul benutzt weitere Module der WK-Bibliothek:Automaton
undEventDispatcher
.wait
: Eine asynchrone Funktion, die in der WK-Biliothek bereitsteht, um eine gewisse Zeitspanne zu warten, bevor eine Aktion durchgeführt wird. Hier wird sie verwendet, um den Start der App ein paar Millisekunden zu verzögern, nachdem die App vollständig geladen und sichbar gemacht wurde.- PixiJS: Eine sehr mächtige 2D-Grafik-Bibliothek, die sehr modular aufgebaut ist. Aus dieser Biliothek werden zwei Klassen verwendet (auch diese verwenden – wie die
GameLoop
– zahlreiche weitere Module, um ihre Aufgaben zu erfüllen; es würde allerdings das Diagramm vollkommen unlesbar machen, alle diese Module hier aufzuführen):PIXI.Application
: Die PixiJS-Root-Klasse. Das zugehörige Objekt enthält alle Elemente, um graphische Elemente effizient auf einem HTML-Canvas-Element darzustellen.PIXI.Graphics
: Eine PixiJS-Klasse zum Zeichnen einfacher geometrischer Formen wie Kreise, Rechtecke, Linien etc.
app
: Das Hauptmodul. Es stellt mit Hilfe der zuvor genannten Module die Spielumgebung zur Verfügung: Eine PixiJS-Bühne, eine Game Loop sowie eine CSS-Datei. Sobald die Umgebung erstellt und initialisiert wurde, startet diese Modul leicht verzögert die Update- und die Render-Funktion des eigentlichen Spielmodulsgame
.game
: Das immer noch molochartige Spielmodul, das im Rahmen des Praktikums in diverse Einzelmodule aufgeteilt werden wird.
Aufgabe 1: Modularisierung ders Game-Moduls
Ein wichtiges Programmierprinzip besagt, dass jedes Modul nur eine Aufgabe erledigen soll. Bislang ist das Game-Modul in dieser Hinsicht noch ziemlich schlecht, da es zahlreiche Aufgabe erfüllt. Dies sollen Sie ändern.
Als Ersatz für das Game-Modul sollen insgesamt sechs Komponenten erstellt werden „Bühne (Model)“, „Ball (Model)“, „Ball (View)“, „Kollissionserkennung und -behandlung (Model)“, „Update-Funktion (Model)“, „Render-Funktion (View)“. Oben sehen Sie das zugehörige Klassendiagramm, bestehend aus insgesamt sieben Modulen:
- Modul
app
: Zuständig für die Initialisierung der Spielumgebung und das anschließende Starten des Spiels. Dieses Modul besteht aus einer Folge von Anweisungen, die direkt zum Start der Web-App ausgeführt werden. - Klasse
ModelStage
: Jedes Objekt dieser Klasse repräsentiert das Modell einer Spielbühne. Üblicherweise gibt es nur eine Spielbühne. Es gibt aber auch Spielsituationen mit mehreren Spielbühnen (z. B. wenn neben der Hauptbühne, die nur einen Ausschnitt der Spielwelt zeigt, eine Minimap existiert, die einen Überblick auf die Spielwelt gewährt). Diese Klasse kann in vielen Anwendungen wiederverwendet werden. Im Laufe der Zeit werden allerdings sicher noch weitere Attribute und Methoden ergänzt werden. - Klasse
ModelCircle
: Eine ebenfalls sehr gut wiederverwendbare Klasse, die für kreisförmige Objekte aller Art in einem Spiel zum Einsatz kommen kann. Normalerweise gibt es zahlreiche kreisförmige Objekte in einem Spiel. Auch hier wird es im Laufe der Zeit sicher diverse Erweiterungen geben. - Funktion
collisionCircleStage
: Ein Modul, das eine Funktion zur Kollissionserkennung und -behandlung von (beweglichen) Kreisobjekten mit (unbeweglichen) Rändern der Bühne zur Verfügung stellt. Auch dieses Modul kann sehr gut wiederverwendet werden. Allerdings gibt es (wie bei allen Fragen der Kollissionserkennung und -behandlung) noch zahlreiches Verbesserungspotential. - Funktionssammlung
update
: Dieses Modul enthält zwei Funktionen:initUpdater
undupdate
. Die erste Funktion dient dazu, die Update-Funktion zu initialisieren, d. h., ihr die Objekte bekannt zu geben, die sie regelmäßig aktualisieren soll. Die zweite Funktion wird der Game Loop als Callback-Funktion übergeben, um diese Aktualisierungen regelmäßig (z. B. genau sechzig mal pro Sekunde) durchzuführen. Außerdem ist die Update-Funktion dafür zusäntdig, die Kollissionserkennung und -behandlung zu initiieren. Sie selbst führt diese Aufgabe allerdings nicht durch, sondern überträgt diese Aufgabe an geeignete Hilfsmodule (in diesem Fall das ModulcollisionCircleStage
). Dieses Modul kann meist nicht problemlos wiederverwendet werden, da jede Web-App ihre ganz eigene Objekt-Welt verwaltet. - Klasse
ViewCircle
: Eine sehr gut wiederverwendbare Klasse, die die kreisförmige Objekte visualisieren soll. In Aufgabe 1 wird der Ball mit Hilfe von PixiJS-Graphics-Befehlen gezeichnet (weshalb die Klasse vielleicht besserViewCircleGraphics
heißen sollte). In einer späteren Aufgabe sollen sie die Visualisierung mit Hilfe von Bildern unter Zuhilfenahme der PixiJS-KlasseSprite
realisieren. (Diese Klasse sollte daher eventuellViewCircleSprite
genannt werden.) - Funktionssammlung
render
: Dieses Modul enthält zwei Funktionen:initRenderer
undrender
. Die erste Funktion dient dazu, die Render-Funktion zu initialisieren, d. h., ihr die Objekte bekannt zu geben, deren Visualisierung sie regelmäßig aktualisieren soll. Die zweite Funktion wird der Game Loop als Callback-Funktion übergeben, um diese Aktualisierungen regelmäßig (möglichst sechzig mal pro Sekunde) durchzuführen. Im Gegensatz zur Update-Funktion ist diese Funktion sehr gut wiederverwendbar. Ihre einzige Aufgabe ist es, für alle View-Objekte, die in einen Array enthalten sind (das ihr bei Initialisierung übergeben wurde), jeweils die Update-Funktion aufzurufen, die jedes View-Objekt bereitstellen muss.
Erstellen der Modul-Struktur
Legen Sie zunächst zwei Ordner an:
src/js/app01/model
src/js/app01/view
Erzeugen Sie dann innerhalb des Ordners src/js/app01/
für jedes Modul eine leere EcmaScript-Datei:
app.js
(diese Datei gibt es schon, sie wird im Folgenden allerdings modifiziert.model/ModelStage.js
model/ModelCircle.js
model/collisionCircleStage.js
model/update.js
view/ViewCircle.js
view/render.js
Die Idee, jedes Modul in einer eigenen Datei zu platzieren, geht auf Java zurück. Im Gegensatz zu anderen Sprachen erzwingt Java, dass jede Klasse in eine eigene Datei geschrieben werden muss (vgl. Why is each public class in a separate file?).
Außerdem gibt es in Java nur eine Art von Modulen: Klassen. Beide Restriktionen gelbt für JavaScript nicht. Sie sollten sich aber angewöhnen, zumindest die Regel „ein Modul === eine Datei mit demselben Namen plus der Endung .js
“ zu beachten.
Fügen Sie in jede Datei einen geeigneten Rahmencode ein. In ein Modul, das eine Klasse enthält (siehe obiges Diagramm), fügen Sie bitte folgenden Code ein,
wobei Sie jeweils CLASSNAME
durch den Klassenname ersetzen müssen.
class CLASSNAME {
constructor() {
}
}
export default CLASSNAME;
oder auch (je nachdem, welche Klammerstruktur sie bevorzugen; bei der folgenden Struktor müssen Sie den Code allerdings Strg-Shift-v
→ Paste without Formating
einfügen, damit sie erhalten bleibt)
class CLASSNAME
{ constructor()
{
}
}
export default CLASSNAME;
Für Funktionsmodule, die mehr als eine Funktion exportieren (hier sind das die Module update
und render
; siehe oben), sollten Sie folgenden Code einfügen (wobei Sie die Funktionsnamen natürlich geeignet ersetzen müssen):
FUNKTIONSNAME_1()
{
}
FUNKTIONSNAME_2()
{
}
export { FUNKTIONSNAME_1, FUNKTIONSNAME_2 };
Sie könnten auch die Arrow-Schreibweise verrwenden. Beachten Sie, dass Sie später diese Funktionen ( z. B. in der Datei app.js
) mittels
import { FUNKTIONSNAME_1, FUNKTIONSNAME_2 } from './ORDNER/MODULNAME.js';
importieren können.
Für Funktionsmodule, die mehr nur eine Funktion exportieren (hier ist das das Modul collisionCircleStage
), sollten Sie folgenden Code einfügen (wobei Sie den Funktionsnamen natürlich geeignet ersetzen müssen):
FUNKTIONSNAME()
{
}
export default FUNKTIONSNAME;
Da hier die Funktion als Default-Objekt exportiert wird, können Sie diese Funktion später mittels
import FUNKTIONSNAME from './ORDNER/MODULNAME.js';
importieren. Hier können bzw. müssen Sie beim Importieren also auf die geschweiften Klammern verzichten. Sollten Sie die geschweiften Klammern unbedingt verwenden wollen, müssten Sie die Exportanweisung anlog zum Modul mit mehreren Funktionsobjekten definieren.
Starten Sie nun npm run watch
und überprüfen Sie , ob die Web-App app01
ausführbar ist. Das sollte auf jeden Fall funktionieren, da Sie ja an der App (d. h. an den Dateien
app.js
und game.js
bislang garnichts geändert haben.
Importieren Sie nun die fünf Module, auf die das Modul app
im obigen Diagramm verweist,
in die Datei app.js
. Beachten Sie, dass die Syntax der Importanweisungen von den Exportanweisungen abhängen,
die im jeweiligen Modul verwendet wurde (siehe Anmerkungen zum Funktionscode zuvor).
Wenn Sie keine syntaktischen Fehler gemacht haben, sollte webpack die App immer noch fehlerfrei übersetzen und ausführen können. WebStorm warnt Sie allerdings bei den Iprot-Anweisungen, dass Sie Objekte improtieren, die Sie gar nicht verwenden. Das stimmt derzeit ja auch, also ist alles in Ordnung.
Implementierung der Komponenten
ModelStage
Implementieren Sie zunächst die Klasse StageModel
. Im UML-Klassendiagramm sehen Sie, dass jedes Objekt diese Klasse vier Attribute hat:
left
, right
, top
, bottom
.
Es sollen jeweils Zahlen darin gespeichert werden. Allerdings ist JavaScript untypisiert, so dass Sie diese
Bedingungen höchstens in JSDoc-Kommentaren formulieren können. Erschwerend kommt hinzu,
das man in EcmaScript-Klassen derzeit Attribute nur mit Hilfe von Getter- und Setter-Methoden definieren kann.
Direkt kann man sie zurzeit nur mit Hilfe des Konstruktors erzeugen.
Also implementieren Sie nur den Konstruktor:
constructor({left: p_left = 0,
right: p_right = 0,
top: p_top = 0,
bottom: p_bottom = 0
})
{ this.left = p_left;
this.right = p_right;
this.top = p_top;
this.bottom = p_bottom;
}
Hier kommt eine Parametersyntax zum Einsatz, die in EcmaScript 6 unter dem Namen Destructuring eingeführt wurde.
In EcmaScript 5 hätten Sie das noch folgendermaßen schreiben müssen:
constructor(p_config)
{ this.left = p_config.left == null ? 0 : p_config.left;
this.right = p_config.right == null ? 0 : p_config.right;
this.top = p_config.top == null ? 0 : p_config.top;
this.bottom = p_config.bottom == null ? 0 : p_config.bottom;
}
In beiden Fällen erwartet der Konstruktor ein Objekt als Argument, das die Initialwerte für die vier Attribute enthält. Sollte ein Wert nicht angegeben werden,
so wird er mit dem Defaultwert 0
initialisiert.
Probieren Sie aus, ob ihr Code funktioniert. Fügen Sie in die Datei app.js
folgende Befehle ein und sehen Sie nach,
was im Konsolfenster des Browsers ausgegeben wird, wenn Sie die Web-App starten.
const
c_model_stage =
new ModelStage
({ "left": 0,
"right": document.documentElement.clientWidth,
"top": 0,
"bottom": document.documentElement.clientHeight
});
console.log(c_model_stage);
Den console.log
-Befehl sollten Sie anschließend wieder löschen, der wird nicht mehr benötigt.
Die Konstante sollte dagegen bestehen bleiben.
Übrigens, um die gewünschten Kommentare können Sie JSDoc-konform folgendermaßen in die Datei einfügen:
Folgender Kommentar sollte vor der Definition der Klasse stehen.
/**
* @class ModelView
*
* @property {number} left - x position of the left border
* @property {number} right - x position of the right border
* @property {number} top - y position of the top border
* @property {number} bottom - y position of the bottom border
*/
Und folgender Kommentar sollte vor der Definition des Konstruktors stehen:
/**
* @param {Object} p_config
* @param {number} [p_config.left = 0]
* @param {number} [p_config.right = 0]
* @param {number} [p_config.top = 0]
* @param {number} [p_config.bottom = 0]
*/
ModelCircle
Implementieren Sie die Klasse ModelCircle
analog zur Klasse ModelStage
.
Im Model dieser Klasse wird allerdings noch die Methode update
mit dem Inputparameter p_delta_s
aufgeführt.
Implementieren Sie diese Methode ebenfalls. Im Rumpf der Methode müssen Sie die ersten beiden Zeilen der Funktion update
einfügen,
die Sie in der Datei game.js
vorfinden.
Testen Sie diese Klasse, indem Sie folgende Konstante in die Datei app.js
einfügen:
const
c_model_ball =
new ModelCircle
({ "r": 75,
"x": 75,
"y": 75,
"vx": 400,
"vy": 300
});
Fügen Sie anschließend folgende Befehle in app.js
ein und überprüfen Sie im Konsolfenster des Browsers, ob alles funktioniert.
console.log(c_model_ball);
c_model_ball.update(1/60);
console.log(c_model_ball);
Diese drei Testbefehle sollten Sie anschließend wieder löschen.
collisionCircleStage
update
ViewCircle
render
Quellen
- Kowarschick (MMProg): Wolfgang Kowarschick; Vorlesung „Multimedia-Programmierung“; Hochschule: Hochschule Augsburg; Adresse: Augsburg; Web-Link; 2018; Quellengüte: 3 (Vorlesung)