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

aus GlossarWiki, der Glossar-Datenbank der Fachhochschule Augsburg
Wechseln zu:Navigation, Suche
(Erstellen der Modul-Struktur)
(Erstellen der Modul-Struktur)
Zeile 175: Zeile 175:
 
in die Datei <code>app.js</code>. Beachten Sie, dass die Syntax der Importanweisungen von den Exportanweisungen abhängen,  
 
in die Datei <code>app.js</code>. Beachten Sie, dass die Syntax der Importanweisungen von den Exportanweisungen abhängen,  
 
die im jeweiligen Modul verwendet wurde (siehe Anmerkungen zum Funktionscode zuvor).
 
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.
  
 
====Modularisierung der Model-Komponenten====
 
====Modularisierung der Model-Komponenten====

Version vom 29. November 2018, 13:34 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 noch nicht online)

1 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.

2 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

3 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.

3.1 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“.

Initiales Moduldiagramm von app01

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 von index01.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 von app.js importiert. Mittels webpack wird dafür gesorgt, dass (die transformierte Version von) der Inhalt der CSS-Datei in index01.html von app.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 und EventDispatcher.
  • 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 Spielmoduls game.
  • game: Das immer noch molochartige Spielmodul, das im Rahmen des Praktikums in diverse Einzelmodule aufgeteilt werden wird.

3.2 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.

geplantes Klassendiagramm von app01

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 und update. 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 Modul collisionCircleStage). 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 besser ViewCircleGraphics heißen sollte). In einer späteren Aufgabe sollen sie die Visualisierung mit Hilfe von Bildern unter Zuhilfenahme der PixiJS-Klasse Sprite realisieren. (Diese Klasse sollte daher eventuell ViewCircleSprite genannt werden.)
  • Funktionssammlung render: Dieses Modul enthält zwei Funktionen: initRenderer und render. 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.

3.2.1 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-vPaste 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.

3.2.2 Modularisierung der Model-Komponenten

Klassendiagramm 1 mit Funktionen, Attributen und Methoden

Gehen Sie folgendermaßen vor:

  • Kopieren Sie den Inhalt der Datei game00.js in die Datei game01/game.js, ohne die darin enthaltenen Import-Befehle zu überschreiben. (Wenn Sie jetzt index01.html im Browser öffnen, sollte der Ball über die Bühne flitzen – zumindest, wenn Sie nicht vergessen haben, grunt watch zu aktivieren :-) )
  • Lagern Sie den Code der Kollissionserkennung und -behandlung aus der Funktion modelUpdates in die Funktion collision in der Datei model/collision.js aus. (Achtung: Der Code zur Berechnung der neuen Position und neuen Geschwindigkeit des Balls darf nicht in diese Datei ausgelagert werden.) Beachten Sie, dass die Funktion collision die Inputparameter p_immovable und p_movable besitzt. Die im kopierten Code enthaltenen Variablennamen v_ball und v_stage müssen entsprechend umbenannt werden.
  • Rufen Sie in der Funktion modelUpdates die neu erstellte (und von game.js auch schon importierte) Funktion collision geeignet auf. Bei einem Test der Web-Anwendung sollte sich der Ball wieder wie gewohnt über die Bühne bewegen.
  • Erstellen Sie nun die Klasse Stage. Gemäß dem obigen Diagramm hat sie sechs Attribute: width, height, lft, rgt, top und btm. Jedes dieser Attribute kann entweder als Wert geseichert oder mit Hilfe von Getter- und Setter-Methoden berechnet werden. Die Entscheidung darüber, wie man vorgehen sollte, hängt davon ab, wie oft man lesend auf die Attribute zugreift und wie oft sie sich verändern. Bei einer Bühne ändern sie sich (nachdem die Bühne einmal initialisiert wurde) meist gar nicht oder nur selten (wenn z. B. ein Eventhandler nach dem Skalieren des Browserfensters die Bühnengröße automatisch anpasst). Gelesen werden die Attribute dagegen oft (bei jeder Kollissionserkennung und -behandlung eines beweglichen Objektes mit der Bühne). Daher bietet es sich hier an, alle Attribute in (internen) Objektvariablen zu speichern. Erstellen Sie also in der Klasse folgenden Konstruktor:
constructor()
{
  this.v_lft = 0;
  this.v_rgt = 0;
  this.v_top = 0;
  this.v_btm = 0;

  this.v_width  = 0;
  this.v_height = 0;
}
  • Dieser Konstruktor legt für jedes der gewünschten Attribute ein passendes objektinternes Attribut an und initialisiert es mit dem Wert 0. Da es in ECMAScript bislang noch keine privaten Klassenmitglieder gibt, kann man bei jedem Bühnenobjekt, das man später anlegt, auch auf diese Attribute zugreifen. Das sollten Sie aber tunlichst lassen. Die Namen wurde extra so gewählt, dass man erkennt, dass es sich eigentlich um private Elemente des Objekts handelt. Um lesend auf die Attribute zugreifen zu können, müssen Sie als nächstes für jedes Attribut eine passende Getter-Methode hinter dem Konstruktor einfügen:
get width()  { return this.v_width; }
get height() { return this.v_height; }

get lft() { return this.v_lft; }
get rgt() { return this.v_rgt; }
get top() { return this.v_top; }
get btm() { return this.v_btm; }
  • Nun können die Attribute des Bühnenobjektes noch nicht aktualisiert werden. Wenn die Werte nur bei Spielstart initialisiert werden, könnte man diese Aufgabe dem Konstruktor übertragen. Allerdings kann sich untergewissen Umständen die Bühnengröße im Laufe der Zeit ändern. Daher ist es sinvoll, auch geeignete Setter-Funktionen für die sechs Attribute zu definieren. Und hier zeigt sich der Vorteil, den der Einsatz von Setter-Funktionen mit sich bringt: Die Werte der Attribute hängen voneinander ab. Zum Beispiel ist die Bühnenbreite gleich dem Abstand zwischen rechtem und linken Rand. Bei der Änderung eines Attributwertes kann man nun automatisch davon abhängige Attributwerte ebenfalls modifizieren, sodass die sogenannten Integritätsbedingungen „Breite gleich Abstand zwischen linken und rechtem Rand“ sowie „Höche gleich Abstand zwischen unterem und oberend Rand“ immer erfüllt sind:
set width (p_w) { this.v_width  = p_w; this.v_rgt = this.v_lft + p_w; }
set height(p_h) { this.v_height = p_h; this.v_btm = this.v_top + p_h; }

set lft(p_x) { this.v_lft = p_x; this.v_width  = this.v_rgt - p_x; }
set rgt(p_x) { this.v_rgt = p_x; this.v_width  = p_x - this.v_lft; }
set top(p_y) { this.v_top = p_y; this.v_height = this.v_btm - p_y; }
set btm(p_y) { this.v_btm = p_y; this.v_height = p_y - this.v_top; }
  • Bislang enthalten alle Attribute eines Bühnenobjekt den Wert 0, nachdem es mit Hilfe des Befehls
     let buehne = new Stage()
    
    erzeugt wurde. Das ist eine ziemlich unbrauchbare Bühne. Mann könnte es nut mittels
    buehne.width = 500; buehne.height = 400;
    
    an die gewünschten Gegebenheiten anpassen. Die Setterfunktionen würden dann nicht nur die Breite und Höhe, sondern auch gleich noch den rechten und den unteren Rand entsprechend anpassen. Dieses Vorgehen ist aber etwas umständlich. Schöner ist es, wenn man die Initialwerte dem neuen Objekt gleich bei dessen Erzeugung übergeben kann:
    new Stage( { width: 500, height: 400 } )
    
    Um dies zu erreichen, übergeben Sie dem Konstrouktor ein Initialisierungsobjekt, dessen Attribute mit Hilfe der zuvor definierten Setter-Funktionen dem Objekt noch während der Erzeugung zugewiesen werden. Ergänzen Sie im Konstruktor den Parameter
    p_config = {}
    
    und fügen Sie dann folgende Schleife[3] als letzten Befehl in den Konstruktor ein:
for (let l_key of Object.keys(p_config))
{
  this[l_key] = p_config[l_key];
}
  • Diese Schleife liest alle Attribute aus dem Initialisierungsobjekt p_config aus und weist sie dem neu erstellten Objekt this unterdemselben Namen als Attribut zu. Wenn das Initialisierungsobjekt korrekt ist und die Attribute mit den Namen width, height, lft, rgt, top bzw. btm enthält, werden die entsprechenden Setterfunktionen aufgerufen. Achung: Es wird nicht sichergestellt, dass nur diese sechst Attributwerte im neuen Bühnenobjekt gespeichert werden. Jedes Attribut, das in p_config übergeben wird, wird ins Bühnenobjekt kopiert. Hier ist wieder Programmierdisziplin gefragt.
  • Ändern Sie nun die Datei game.js geeignet ab. Weisen Sie der Variablen v_stage ein neu erstelltes Bühnenobjekt zu: new Stage(). Da die Bühnengröße innerhalb der Funktion init an die Bildschirmgröß angepasst wird, ist es hier nicht notwendig, dem Konstruktor ein Initialisierungsobjekt zu übergeben. Wenn Sie nun die Anwendung wider starten (grunt nicht vergessen), sollte die Anwendung wieder funktionieren. (Die Klasse Stage wird bereits importiert.)
  • Nun ist es an der Zeit, die Korrektheit der Setter-Funktionen auszuprobieren: Ersetzen Sie in den ersten beiden Befehlen der Funktionen init die Befehle
    v_stage.rgt = p_stage.width; v_stage.btm = p_stage.height;
    
    durch
    v_stage.width = p_stage.width; v_stage.height = p_stage.height;
    
    . Die Anwendung sollte immer noch funktionieren! (Vorsicht, Falle: Wenn Sie vergessen grunt aufzurufen, funktioniert die App ebenfalls weiterhin, da die Bundle-Datei app1.bundle.js nicht neu erstellt wird.)
  • Erstellen Sie nun die Klasse Circle analog. Allerdings gelten nun hinsichtlich der Entscheidung „Attribut speichern oder berechnen“ andere Regeln. Ein Kreis ist beweglich. Entsprechend oft ändern sich seine Position und seine Geschwindigkeit. Auch die Beschleunigung kann sich regelmäßig ändern (z. B., wenn der Benutzer den Ball mit Hilfe eines Eingabegeräts wie Tastatur oder Maus steuert). Und selbst der Radius kann sich häufig ändern (z. B. weil der Ball aufgrund einer Aktion wächst oder schrumpft.)

Bei jeder Änderung die vier Ränder lft, rgt, top und btm, die es laut Datenmodell geben muss (siehe Grafik) neu zu berechnen und zu speichern, kostet zu viel Zeit. Besser ist es den Wert eines dieser Attribute nur dann zu berechnen, wenn dies bei der Kollissionserkennung und -behandlung benötigt wird. Schreiben Sie den Konstruktor also so, dass der Radius, die Positionsattribute, die Geschwindigkeitsattribute und Beschleunigungsattribute direkt gepeichert werden:

this. r = 0;
...
  • Die Gettermethoden für die Ränder der Bounding Box definieren Sie so, dass die jeweiligen Werte mit Hilfe von Mittelplunkt und Radius ermittelt werden:
get lft() { return this.x - this.r; }
...
  • Und die Setter-Methoden definieren Sie so, dass jeweils die [math]x[/math]- bzw. die [math]y[/math]-Position so angepasst wird, dass sich der entsprechende Rand an der gewünschten Position befindet:
set lft(p_x) { this.x = p_x + this.r; }
...
  • Nun müssen Sie noch die Methode update(p_dt = 1/60) definieren. Kopieren Sie dazu die ersten vier Zeilen aus der Methode modelUpdates und fügen Sie diese in die Methode ein. Beachten Sie, dass das Ball-Objekt jetzt nicht mehr v_ball heißt, sondern this.
  • Als nächstes sollten Sie die Datei game.js anpassen, so dass die neu erstellte Kalsse zum Einsatz kommt. Weisen Sie der Variablen v_ball ein neu erstelltes Kreisobjekt zu: new Circle({r: 75, ...}). Als Initialisierungsobjekt verwenden Sie das Objekt, das bislang als Kreisobjekt in der Datei game.js verwendet wird. Außerdem sollten Sie die ersten vier Anweisungen in der Funktion modelUpdates durch einen Methodenaufruf ersetzen:
v_ball.update(p_dt);
  • Das heißt, die Funktion modelUpdates erledigt ihre Aufgabe „Ball bewegen und Kollissionen erkennen und behndeln“ an sofort nur noch mittels zweier Hilfsfuktionen, der Funktion collision und der Methode Circle.update. Testen Sie, ob die Web-Anwendung noch funktioniert.
  • Zu guter Letzt sollten Sie noch eine Unsauberkeit in der Datei collision.js bereinigen. In der Kollissionsfunktion wird auf das kreisspezifische Attribut r zugegriffen. Doch nicht jedes bewegliche hat einen Radius. Seitdem Sie die Klasse Circle definiert haben, hat jedes Kreisobjekt zusätlich die Attribute lft, rgt, top und btm. Diese Attribute können nicht nur gelesen, sondern auch geändert werden. Schreiben Sie den Code im Rumpf der Funktion colission so um, dass das Attribut r gar nicht mehr verwendet wird.

3.2.3 Modularisierung der View-Komponenten

Erstellen Sie eine Klasse ViewCircle mit dem Konstruktor constructor(p_pixi, p_canvas, p_model, p_config = {}) zur Erzeugung eines Viewobjekts für den Ball. In der Datei game.js wird dieses Objekt in der Variablen v_ball_view gespeichert. Das eigentliche Viewobjekt wird derzeit in der Initialisierungsfunktion init erzeugt und initialisiert. Mit Hilfe Ihrer Klasse sollte dazu nur noch ein Befehl notwendig sein:

v_ball_view
  = new ViewCircle(p_pixi, p_canvas, v_ball,
                   {
                     border:      5,
                     borderColor: 0xAAAAAA,
                     color:       0xFFAA00
                   }
                  );

Ein Viewobjekt muss zwei Attribute anbieten, da die Funktion viewUpdates n der Datei game.js darauf zugreift: x und y. Diese Attribute müssen einen Zugriff auf das zugehörige Pixi-Sprite gewähren, welches innerhalb des Viewobjekt gespeichert wird. Dazu sind je eine Getter- und eine Setter-Methode vorzüglich geeignet.

Wenn Sie in der Klasse ViewCircle auch noch eine Update-Methode implementieren – analog zur Update-Methode in der Klasse MovableCircle –, können Sie die Funktion viewUpdates sogar noch vereinfachen. Sie brauchen nur noch die neu definierte Updatemethode geeignet aufzurufen. (Anmerkung: Nachdem Sie dies gemacht haben, sind die beiden zuvor definierten Attribute x und y eigentlich überflüssig.)

4 Quellen

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