MMProg: Praktikum: WiSe 2017/18: Ball03b
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 | Game Loop 01 | Ball 02 | Ball 03 | Ball 03b | Pong 01
Musterlösung: SVN-Repository
Ziel
In diesem Praktikum wird die im dritten Teils des Tutoriums begonnene Modularisierung des Projekts aus dem zweiten Teils des Tutoriums weitergeführt.
Aufgaben
Laden Sie das leere Projekt WK_Ball03b_empty auf Ihren Rechner.
Installieren Sie aber nicht die Node.js-Module, das machen Sie später. Sie finden das leere Projekt im
Repository-Pfad https://glossar.hs-augsburg.de/beispiel/tutorium/es6
im Unterordner empty
.
Erstellen Sie ein neues Projekt praktikum03b
und kopieren Sie die Ordner src
und web
(samt Inhalt)
sowie alle Dateien, die Sie im Wurzelverzeichnis des Projekts WK_Ball03b_Empty
finden, mittels Ctrl-/Apfel-C
Ctrl-/Apfel-V
in Ihr eigenes Projekt. (Die Frage, ob WebStorm seinen eigenen File Watcher zum
Übersetzen von ES6-Code in ES5-Code verwenden soll, beantworten Sie bitte mit „No“. Das erledigt Webpack für Sie.)
Sie können Ihr Projekt zur Übung auch im Subversion-Repository speichern. Das ist aber nicht so wichtig.
Nun können Sie in Ihrem eigenen Projekt die benötigten Node.js-Module installieren: npm i
.
In Ihrem Projekt finden Sie wiederum mehrere Web-Anwendungen: index01.html
verwendet die gepackte Version von app01.js
,
die ihrerseits das Spiel game01/game.js
einbindet. Et cetera. Die Web-Anwendung app00
basiert auf dem
nicht-modularen Spiel game00.js
, das in der vorherigen Praktikumsaufgabe teilweise modularisiert wurde. Eine Musterlösung
der Praktikumsaufgabe MMProg: Praktikum: WiSe 2017/18: Ball03 finden Sie im Ordner src/js/app/game01
vor.
Sie können zur Lösung der nachfolgenden Aufgaben natürlich auch auf IHre eigene Lösung der letzten PRaktikumsaufgabe zurückgreifen.
Schreiben Sie Ihre Lösungen der Aufgabe $ i $ in die Ordner game
$ i $.js
.
Versuchen Sie möglichst viele Module der jeweils vorangegangenen Aufgabe wiederzuverwenden.
Aufgabe 1: Analyse der vorhandenen Web-Anwendungen
Machen Sie sich noch einmal klar, wie sich die Web-Anwendungen app00
und app01
unterscheiden.
Da der Code für beide Anwendungen in Ihrem Projekt bereits enthalten ist, brauchen Sie hier nichts zu implementieren.
Aufgabe 2: Weitere Modularisierung der Web-Anwendung
Diese Aufgabe verfolgt zwei Ziele: Zum einen soll die Moularisierung voranschreiten und zum anderen soll JSON zum Initialisieren der Anwendung zum Einsatz kommen.
Wiederverwendung von Elementen der alten Web-Anwendung
Im ersten Schritt kopieren wir Elemente der alten Web-Anwendung game01
in die neue game02
(Wiederverwendung).
Da aber noch keine optimale Modularisierung erfolgt ist, geht das leider nicht ohne zusätzlichen Anpassungen.
- Die Datei
game01/model/collision.js
können Sie vollständig übernehmen. - Die in der Klasse
Circle
(game01/model/Circle.js
) enthaltenen Methoden können Sie ebenfalls übernehmen. Beacten Sie allerdings, dass die Klasse jetztMovableCircle
heißt. Sie dürfen in der Dateigame01/model/MovableCircle.js
den Klassennamen und den Export-Befehl nicht ändern. Sie sollten in den vorgegebenen Klassenrahmen lediglich die kopierten Funktionen einfügen. Wenn Sie nun Grunt laufen lassen, werden Sie feststellen, dass ein Fehler gemeldet wird. Der Grund dafür ist, dassMovableCircle
als Subklasse vonMovable
deklariert wurde (MovableCircle extends Movable
), Der Konstruktor einer Subklasse muss immer den Konstruktor der zugehörigen Superklasse aufrufen. Dieser Aufruf muss als erster Befehl im Konstruktor stehen. Fügen Sie also den Befehlsuper();
in den Konstruktor vonMovableCircle
ein. Jetzt sollte Grunt den Code wieder fehlerfrei übersetzen können. - Die Datei
game01/model/Stage.js
können Sie wieder vollständig übernehmen. Diese Datei wird erst in der nächsten Aufgabe angepasst. - Ebenso können Sie die Datei
game01/view/ViewCircle.js
in das neue Projekt kopieren. Diese Datei wird später noch etwas vereinfacht, aber notwendig ist das nicht unbedingt. - Zu guter Letzt können Sie die
let
-Anweisung und die drei Funktionsdefinitionrninit
,modelUpdates
undviewUpdates
aus der Dateigame01/game.js
ind die Dateigame02/game.js
kopieren (ohne die Imporbefehle und den Exportbefehl zu verändern). Grunt sollte jetzt das Projekt übersetzen können, aber wenn Sie es starten, sehen Sie nur eine leere Bühne. Sie müssen in dieser Datei (game02/game.js
) noch ein paar Anpassungen vornehmen:new Stage
muss durchnew ModelStage
ersetzt werden, da die KlasseStage
jetzt unter diesem Namen importiert wird.new Circle
muss durchnew ModelCircle
aus dem gleichen Grund ersetzt werden.- Die Init-Funktion muss angepasst werden:
- Sie erwartet andere Parameter (vergleichen Sie die Parameterlisten von
init
in den Klassendiagrammen von app01 und app02). Ersetzen Sie also die Parameterliste voninit
durch die korrekte Parameterliste.p_pixi, p_canvas, p_config, f_ready
- Sie Parameter
p_pixi
undp_canvas
gibt es immer noch, der Parameterp_stage
ist weggefallen (er hatte die Größenparameter der Bühne enthalten), dafür sindp_config
undp_ready
hinzugekommen. Im Parameterp_config
übergibt die Hauptanwendungapp02
den Inhalt der Konfigurationsdateijson/config02.json
. In diesem Objekt sind insbesondere auch die Größenparameter der Bühne enthalten. Und die Funktionf_ready
muss aufgerufen werden, sobald die Initialisierung vollständig erfolgt ist (vgl. Praktikumsausgabe „Ball02“, Aufgabe 3). - Passen Sie daher die die ersten beiden Befehle der Init-Funktion an: sowie
v_stage.width = p_config.model.stage.width;
v_stage.height = p_config.model.stage.height;
- Fügen Sie nun noch den Aufruf
f_ready();
als letzten Befehl in die Init-Funktion ein. Nun sollte die Anwendung (nach einer Übersetzung mittels Grunt) im Browser wieder laufen.
- Sie erwartet andere Parameter (vergleichen Sie die Parameterlisten von
JSON
Wie man eine JSON-Datei lädt und darauf zugreift wurde ebenfalls schon in der Praktikumsausgabe „Ball02“, Aufgabe 3 behandelt. Allerdings wurde dort die JSON-Datei direkt von der Datei game01.js
importiert. Hier wird die JSON-Datei json/config02.json
dagegen
von der App-Datei app02.js
importiert und dann an die Init-Funktion in der Datei game02/game.js
weitergegeben.
Dieses Vorgehen bringt mehrere Vorteile mit sich:
- Man kann auf spezielle Konfigurationsparameter wie
p_stage
verzichten. Die App-Datei übergibt alle Konfigurationsinformationen in einen einzigen Objekt (namensp_config
) an die Init-Funktion. - Die App-Datei liest die Konfigurationsinformationen üblicherweise aus einer JSON-Datei ein. Sie kann das JSON-Objekt aber noch um Informationen ergänzen, die erst zu Laufzeit feststehen, wie z. B. die aktuelle Größe der Bühne. Schauen Sie sich die Datei
json/config02.json
einmal an. Darin stehen im Stage-Objekt keine knkreten Größenangaben, sondern die Zeichenketten"@width"
und"@height"
. Diese werden, nachdem das JSON-Objekt innerhalb der Dateiapp02
geladen wurde, mit Hilfe der Funktionconcretize
(„konkretisiere“) durch die aktuelle Größe des Browserfensters ersetzt. Diese Funktion finden Sie in der Dateilib/wk/Util.js
. Sie ist noch wesentlich mächtiger, wie sie im Laufe des Praktikums lernen werden. Sie kann z. B. die Position, Geschwindigkeit oder Beschleunigung eines beweglichen Objektes mit zufälligen Werten initialisieren. Sehen Sie sich doch einfach einmal die anderen JSON-Dateien im Ordnerjson
an, damit Sie einen Eindruck davon bekommen, für was man diese Funktion noch einsetzen kann. (Sie müssen die einzelnen @-Anweisungen jetzt noch nicht verstehen; geldulden Sie sich noch etwas.) - Man könnte problemlos eine zweite Anwendungsdatei
app02a
schreiben, die einfach eine andere JSON-Datei einliest, aber wieder dieselbe Dateigame02/game.js
einbindet, um das Spiel auszuführen. Das heißt, man kanngame02/game.js
für ganz viele verschiedene Konfigurationsdateien wiederverwenden, ohne in den Dateien im Ordnergame02
eine einzige Zeile Code ändern zu müssen. Üblicherweise würde man natürlich nicht lauer verschiedene Anwendungsdateienapp02a
,app02b
,app02c
,app02d
etc. erstellen, die jeweils nur eine andere JSON-Datei einlesen würde (das wäre wahrlich nicht DRY), sondern man würde einen Level-Mechanismus programmieren, der jedesmal, wenn ein Level fertiggespielt wurde, die JSON-Datei des nächsten Levels einlesen und dann das Spiel mit dieser Konfigurationsdatei neu starten würde.
Damit der soeben beschrieben Vorteil der Konfigurationsdatei config02.json
auch zum tragen kommt, muss man die darin vorgegebene
Werte natürlich auch in der Datei game.js
verwenden.
Ersetzen Sie in der let
-Anweisung die Definition
v_ball = new ModelCircle({ r: 75, ... }),
durch
v_ball = null,
und fügen Sie dafür den Befehl
v_ball = new ModelCircle(p_config.model.ball)
in die Initfunktion ein. Ersetzen Sie außerdem in der Initfunktion den Befehl
v_ball_view
= new ViewCircle(p_pixi, p_canvas, v_ball,
{ border: 5,
borderColor: 0xAAAAAA,
color: 0xFFAA00
}
);
durch den Befehl
v_ball_view = new ViewCircle(p_pixi, p_canvas, v_ball, p_config.view.ball);
Nun können Sie die Werte in der Datei config02.json
probehalber einmal ändern. Es sollte sich
auch das Verhalten und oder das Aussehen des Balls auf der Bühne entsprechend ändern. Sie können versuchsweise
auch einmal die Größe der Bühne auf feste Maße festlegen.
Jetzt können Sie versuchshalber eine Web-Anwendung index02a.html
plus app02a
erstellen, die dasselbe Spiel game02
mit einer anderen JSON-Datei ausführt. Vergessen Sie nicht, diese Anwendung in die die Datei webpack.config.js
einzutragen und Grunt anschließend neu zu starten. Sehen Sie sich einmal die Musterlösung zun dieser Aufgabe an. Dort wurde eine Bühne mit Rand defieniert, die mit Hilfe von CSS in die Mitte des Bildschirms verschoben wurde. Das funktioniert natürlich auch für eine Bühne fester Größe. (Nicht für jedes
Spiel ist eine Bühne geeignet, deren Größe von der Größe des Brwoserfesnters abhängt.)
Modularisierung
So, nun ist es an der Zeit, die fehlenden Module zu erstellen. Wenn Sie die beiden obigen Diagramme vergleichen, stellen
Sie fest, dass drei zusätzliche Module eingeführt werden: modelUpdate
, viewUpdate
und
die Klasse Movable
.
Beginnen Sie mit der Klasse Movable
.
Wenn man sich die Klasse MovableCircle
genauer ansieht, enthält sie viele Attribute, die auch in anderen geometrischen Objekte
benötigt werden. Im Prinzip besteht die Klasse aus einer Bounding Box (Sie sollten diesen Artikel lesen, um die Bedeutung der Bounding-Box-Attribute zu verstehen.),
Geschwindigkeits- und Beschleunigungsattributen sowie einem Radius. Nur der Radius ist kreisspezifisch.
Alle anderen Attribute benötigt jedes bewegliche Objekt. Also lagern Sie sie in eine Klasse names Movable
aus,
von der die Klasse MovableCircle
und später auch diverse andere geometsiche Objektklassen alle Movable-Attribute erben.
Die Klasse MovableCircle
fügt dieser Liste nur noch das kreisspezifische Attribut r
(Radius) hinzu.
Die Klasse Movable
kann folgendermaßen definiert werden:
constructor(p_config = {})
{ mixin
( // Default values
{ x: 0, y: 0,
vx: 0, vy: 0,
ax: 0, ay: 0,
width: 0, height: 0,
xPivot: 0, yPivot: 0
},
mixin(p_config, this)
);
this.type = 'Movable';
}
get lft() { return this.x - this.xPivot; }
get rgt() { return this.lft + this.width; }
get top() { return this.y - this.yPivot; }
get btm() { return this.top + this.height; }
set lft(p_x) { this.x = p_x + this.xPivot; }
set rgt(p_x) { this.x = p_x + this.xPivot - this.width; }
set top(p_y) { this.y = p_y + this.yPivot; }
set btm(p_y) { this.y = p_y + this.yPivot - this.height; }
update(p_dt = 1/60)
{ this.x += this.vx * p_dt;
this.y += this.vy * p_dt;
this.vx += this.ax * p_dt;
this.vy += this.ay * p_dt;
}
}
Der Code sieht nicht viel anders aus, als der Code, der derzeit in der Klasse
MovableCircle
enthalten ist. Die Getter- und Setter-Methoden
der Attribute lft
, rgt
, top
und btm
greifen nicht mehr auf den Radius zu, da es diesen nicht mehr gibt. Ihre
Implementierung ergibt sich aus den Integritätsbedingungen, die für
Bounding Boxes gelten (vgl. Artikel Bounding Boxes).
Der wesentlich Unterschied liegt in der Verwendung der Funktion mixin
In der Datei MovableCircle
werden bislang die Konfigurationsatriute aus dem
Objekt p_config
mittels der Scheife
for (let l_key of Object.keys(p_config))
this[l_key] = p_config[l_key];
in das neu erstellte Objekt this
kopiert. Diese Aufgabe
übernimmt jetzt der Befehl
mixin(p_config, this)
Die Funktion mixin
wird in der Datei lib/wk/Util.js
folgendermaßen definiert:
function mixin(p_source, p_target, p_replace = false)
{ for (let l_key of Object.keys(p_source))
if (p_replace || !p_target[l_key])
p_target[l_key] = p_source[l_key];
return p_target;
}
Sie funktioniert also im Prinzip wie die obige Schleife: Sie kopiert die Attribute des Objektes p_source
in das Objekt p_target
. Allerdings gibt es noch einen weiteren Parameter: p_replace
.
Wenn dieser den Wert true
hat, kopiert er ohne Rücksicht auf Verluste alle Attribute von
p_source
nach p_target
. Defaultmäßig ist er jedoch auf false
gesetzt. Das bedeutet, dass nur diejenigen Attribute von p_source
nach p_target
kopiert werden, die in p_target
noch nicht exitieren.
mixin
gibt das Ziel-Objekt als Ergebnis zurück. Damit ist es möglich, mehrere
mixin
-Aufrufe zu schachteln. Zum Beispiel.
mixin(source1, mixin (source2, mixin (source3, target)));
Dies wir hier ausgenutzt, um für alle Werte, die in p_config
nicht spezifiziert wurden, sinnvolle Defaultwerte in das neu erstellte Objekt einzufügen.
mixin
( // Default values
{ x: 0, y: 0,
vx: 0, vy: 0,
ax: 0, ay: 0,
width: 0, height: 0,
xPivot: 0, yPivot: 0
},
mixin(p_config, this)
);
Beachten Sie, dass keine Werte überschrieben werden, die bereits in p_config
festgelegt wurden,
da p_replace
standardmäßig den Wert false
hat.
Sie können den Konstruktor aber auch auf die schon bekannte Art und Weise definieren. Sie
initialisieren zunächst alle Attribute mit 0
und überschreiben dann diese Defaultwerte
mit den übergebenen Werten aus p_config
.
constructor(p_config = {})
{ this.type = 'Movable';
this. x = 0; this.y = 0;
this.vx = 0; this.vy = 0;
this.ax = 0; this.ay = 0;
this.width = 0; this.height = 0;
this.xPivot = 0; this.yPivot = 0;
mixin(p_config, this, true);
}
Beachten Sie, dass hier den Mixin-Befehl mit dem Parameter true
aufgerufen werden muss,
da er ansonsten die zuvor definierten Defaultwerte nicht überschreiben würde.
Die Klasse MovableCircle
vereinfacht sich nun:
class MovableCircle extends Movable
{ constructor(p_config = {})
{ super
( mixin
( // circle standard values (which may not be replaced)
{ width: 2*p_config.r,
height: 2*p_config.r,
xPivot: p_config.r,
yPivot: p_config.r,
},
mixin ({r: 0}, p_config),
true // replace wrong values within p_config
)
);
this.type = 'MovableCircle';
}
}
Im Prinzip wird nur ein Radius r
hinzugefügt. Sofern im Konfigurationsobjekt p_config
kein Radius vorgegeben wurde,
wird er mit 0
initialisiert: mixin ({r: 0}, p_config)
.
Der äußere Mixin-Befehl bewirkt, dass die Höhe und die Breite des Kreises auf den doppelten Radius festgelegt wrd.
Der Ankerpunkt (xPivot, yPivot)
wird in die Mitte des Kreises gelegt.
Dieselbe Initialisierung könnte auch ohne Mixin-Befehl vorgenommen werden:
class MovableCircle extends Movable
{ constructor(p_config = {})
{ super(p_config);
this.r = p_config.r || 0;
this.width = 2*this.r;
this.height = 2*this.r;
this.xPivot = this.r;
this.yPivot = this.r;
this.type = 'MovableCircle';
}
}
Also wirklich notwendig ist die Mixin-Variante hier nicht. Diese Variante hat allerdings den Vorteil, das
man das Konfigurationsobekt p_config
anpassen kann, bevor" es an die Superklasse übergeben wird.
Das ist auf konventionelle Art nicht möglich, da der Befehl super
stets als erster Befehl im
Konstruktor stehen muss. (Um sich das klar zu machen, sollten Sie überlegen, warum in der zweiten Variante nicht
this.width = 2*p_config.r;
stehen darf, es aber kein Problem ist, in der ersten Variante p_config.r
zur Initialisiertung von this.width
zu verwenden.)
Jetzt müssen noch die beiden Module modelUpdate
und viewUpdate
erstellt werden.
Das Modul modelUpdate
wird künftig die Aufgabe übernehmen, alle beweglichen Objekte um einen Schritt
weiterzubewegen und die Kollissionserkennung- und behandlung für alle auf der Bühne vorhandenen Objekte vorzunehmen.
Zunächst wird im Prinzip nur der Code der Funktion modelUpdate
aus der Datei game.js
hierhin ausgelagert.
model/modelUpdate.js
function modelUpdate(p_models, p_dt = 1/60)
{ const
c_stage = p_models.stage,
c_ball = p_models.ball;
c_ball.update(p_dt);
collision(c_stage, c_ball);
}
Für das Modul viewUpdate
gelten ähnliche Überlegungen. Dieses Modul wird künftig
für alle View-Objekte den View-Update vornehmen. Zunächst wird aber auch hier im Prinzip lediglich der Code
der Funktion viewUpdate
in die Datei game.js
ausgelagert.
view/viewlUpdate.js
function viewUpdate(p_views)
{ p_views.ball.update(); }
Jetzt müssen die beiden Funktionen noch von game.js
aufgerufen werden.
Ersetzen Sie in dieser Datei die let
-Anweisung durch folgende Anweisung:
let
v_models = {},
v_views = {};
Im Objekt v_models
werden künfitg 'alle Modell-Objekte abgelegt
und im Objekt 'v_views
alle View-Objekte.
Diese werden den beiden neu geschaffenen Modulen zu weiteren Behandlung übergeben.
function modelUpdates(p_dt = 1/60)
{ modelUpdate(v_models, p_dt); }
function viewUpdates()
{ viewUpdate(v_views); }
Diese Struktur ist so universell, dass sich sie in alle künftigen Projekten verwendet werden wird.
Jetzt müssen nur noch die Model- und die View-Objekte erzeugt und in den Objekten
v_models
und v_views
abgelegt werden.
Ersetzen Sie dazu die ersten vier Befehle der Init-Methode durch folgende Befehle:
v_models.stage = new ModelStage(p_config.model.stage);
v_models.ball = new ModelCircle(p_config.model.ball);
v_views.ball = new ViewCircle(p_pixi, p_canvas,
v_models.ball, p_config.view.ball
);
Der Befehl f_ready();
bleibt selbstverständlich erhalten.
Wie man sieht, werden alle Objekte sehr einheitlich erzeugt. Das wird
künftig ausgenutzt, um eine beliebige Anzahl von gleichartigen Objekten
(wie z. B. eine Menge von Moorhühnern) verwalten zu können.
Wenn Sie sich die Datei game.js
ansehen, nimmt sie jetzt nur noch eine
Aufgabe wahr. Sie erstellt alle Spielobjekte. Für die Verwaltng der Objekte nimmt sie diverse
andere Module zur Hilfe.
Quellen
- Kowarschick (MMProg): Wolfgang Kowarschick; Vorlesung „Multimedia-Programmierung“; Hochschule: Hochschule Augsburg; Adresse: Augsburg; Web-Link; 2018; Quellengüte: 3 (Vorlesung)