HTML5-Tutorium: Canvas: MiniPong 04: Unterschied zwischen den Versionen

aus GlossarWiki, der Glossar-Datenbank der Fachhochschule Augsburg
Kowa (Diskussion | Beiträge)
Kowa (Diskussion | Beiträge)
Keine Bearbeitungszusammenfassung
 
(129 dazwischenliegende Versionen von 2 Benutzern werden nicht angezeigt)
Zeile 1: Zeile 1:
{{HTML5-Tutorium:Canvas:MiniPong:Menü}}
{{HTML5-Tutorium:Canvas:MiniPong:Menü}}
'''Musterlösung''': <code>[http://glossar.hs-augsburg.de/beispiel/tutorium/html5_canvas/minipong/html5_canvas_minipong_03/WebContent/index.html Minipong 3]</code>
'''Musterlösung''': <code>[https://glossar.hs-augsburg.de/beispiel/tutorium/es5/minipong/WK_MiniPong04/web/index.html index.html]</code>
([http://glossar.hs-augsburg.de/beispiel/tutorium/html5_canvas/minipong/html5_canvas_minipong_03 SVN-Repository])
([https://glossar.hs-augsburg.de/beispiel/tutorium/es5/minipong/WK_MiniPong04/ WK_MiniPong04 (SVN)])


=Ziel: Implementierung von MiniPong mit Schläger, Ball und Punktanzeige=
'''Leeres Projekt''': <code>[https://glossar.hs-augsburg.de/beispiel/tutorium/es5/minipong/WK_MiniPong04_empty/web/index.html index.html]</code>
Im dritten Teil des Tutoriums wird beschrieben, wie man MiniPong mit Hilfe einer einzigen JavaScript-Datei <code>main.js</code> erstellt (wenn man von der Auslagerung der Konstanten in eine
([https://glossar.hs-augsburg.de/beispiel/tutorium/es5/minipong/WK_MiniPong04/ WK_MiniPong04_empty (SVN)])
eigene „Initialisierungsdatei“ einmal absieht).
[[Kowarschick|Wolfgang Kowarschick]] bezeichnet so eine Datei als „Moloch-Datei“ (Moloch = Monstrum, Ungeheuer, alles verschlingende Bestie).


Drei Objekte werden implementiert: Das ROOT-Objekt der Anwendung, ein Ball-Objekt und ein Schläger-Objekt.
==Ziel: Das fertige Spiel „MiniPong“==
[[Medium:MiniPong03Canvas.png|miniatur|ohne|582px|Das Objektmodell von MiniPong 03]]
Im vierten Teil des Tutoriums wird eine funktionsfähige Version von MiniPong erstellt.


Anmerkung: Darüber hinaus gibt es noch ein Info-Objekt, zur Ausgabe von Informationen über den Spielverlauf.
===Use Cases===
Das sich dieses Text-Objekt jedoch nicht auf der Bühne befindet, wurde es im obigen Modell nicht aufgeführt.
[[Datei:MiniPong04 UseCases.png|ohne|882px|Use Cases der Tutoriums-Anwendung <code>MiniPong</code>]]
Das heißt, das obige Modell ist genaugenommen unvollständig.
Ein Spieler kann das Spiel starten (Startknopf) und vorzeitig
beenden (Stopp-Knopf). Nach Spielstart kann der Spieler den Schläger
am unteren Rand des Spielfeldes mit Hilfe der Richtungstasten des
Keyboards nach links und rechts bewegen.


=Anwendung „<code>MiniPongCanvas03</code>“=
Nachdem das Spiel gestartet wurde, bewegt sich
der Ball geradlinig im Spielfeld, wobei die Startrichtung zufällig
gewählt wird. Kollisionen mit der linken, oberen oder
rechten Wand haben eine Richtungsänderung des Balls
zur Folge (Einfallswinkel = Ausfallwinkel).
Eine Kollision mit der unteren Wand beendet das Spiel.
 
Ziel des Spiels sind möglichst viele Kollision des Balls mit
dem Schläger. Eine derartige Kollision hat eine Richtungsänderung
sowie einen Punktgewinn zur Folge. Der aktuelle Punktestand (Score)
wird der Benutzer jederzeit angezeigt.
 
Wird der Schläger im Moment der Kollision bewegt, so wird der Ball abhängig von der
Bewegungsrichtung und Geschwindigkeit des Schlägers abgelenkt (Simulation von Reibung).
 
===Klassenmodell===
[[Datei:MiniPong04 ClassModel Overview.png|gerahmt|rechts|Klassendiagramm]]
 
Eine genauere Analyse der Use Cases zeigt, welche Module benötigt werden. Die verschiedenen Module sind verschieden eingefärbt:
 
* grau: Initialisierung der Anwendung
* blau: Model der Anwendung
* orange: Controller zur Behandlung der Benutzeraktionen (mit Ausnahme von Formularelementen, die in der View enthalten sind)
* flieder: Anwendungslogik
* grün: Anwendungsview
* gelb: Kollisionserkennung und -behandlung
 
Die Kollisionserkennung und -behandlung erhält eine eigene Farbe, da sie eine Mischung aus
Model und Controller ist. Einerseits verändert sie die Bewegungseigenschaften beweglicher Objekte (Ball und Schläger).
Das heißt, sie ist wesentlicher Bestandteil des Models. Andererseits informiert sie die Anwendung über Aktionen
des Balls, damit diese entsprechend reagieren kann. Dies ist eine typische Controllertätigkeit.
 
Das Modul „<code>init</code>“ hat die Aufgabe das Spiel zu initialisieren. Es definiert eine [[Prozedur]]
„<code>init</code>“, die zunächst die wesentlichen Objekte erstellt und dann das Spiel startet.
Im Klassendiagramm sind rote Pfeile zu den Modulen eingezeichnet, die das Init-Modul benötigt.
Das sind die beiden Funktionsmodule „<code>logic/minipong</code>“ und „<code>control/keyboard</code>“
sowie diverse Model- und View-Klassen. Für jede dieser Klassen – mit Ausnahme der Klasse „<code>ModelStage</code>“ –
erzeugt <code>init</code> ein oder im Falle der Text-Klassen sogar jeweils zwei Objekte. Als Stage-Objekt kommt
– wie im [[HTML5-Tutorium: Canvas: MiniPong 03|dritten Teil des Tutoriums]] – das in der HTML-Datei enthaltene
Canvas-Element zum Einsatz.
 
Die View-Objekte sind ausschließlich der Prozedur „<code>init</code>“ bekannt. Diese Objekte visualisieren
die Model-Objekte, {{dh}} den aktuellen Zustand des Spiels. Da sich der Zustand des Spiel häufig ändert,
erstellt <code>init</code> eine View-Loop, deren Aufgabe es ist, die Visualisierung möglichst häufig,
am Besten 60 mal pro Sekunde zu erneuern. Dazu ruft die View-Loop, sobald sie einmal gestartet wurde,
regelmäßig die Zeichenmethode „<code>draw</code>“ der einzelnen View-Objekte auf.
 
[[Datei:MiniPong04 ClassModel ViewLoop.png|gerahmt|rechts|Klassendiagramm: Detailansicht der View-Loop]]
Die View umfasst sechs Elemente:
* einen Button (Start, Stopp)
* einen Ball
* einen Schläger
* zwei Textfelder (Score und allgemeine Informationen)
Die View-Loop muss diese sechs Objekte selbstverständlich kennen, damit sie sie darstellen kann. Allerdings
reicht es, wenn ihr eine Liste von View-Objekten übergeben wird. Die einzige Forderung, die die die View-Loop
an diese Objekte stellt, ist, dass für sie eine Zeichenmethode „<code>draw(p_context)</code>“ defiert wurde,
mittels derer sie die Objekte auf dem Canvas (oder evtl. auch im HTML-Dokument selbst) visualisieren kann.
Das heißt, die View-Klassen, deren Objekte von der View-Loop visualisiert werden sollen, müssen das
[[Schnittstelle_(OOP)#Schnittstelle_.28Interface.29|Interface]]  „<code>View</code>“ implementieren.
 
Die Init-Prozedur erzeugt die sechs View-Objekte sowie die zugehörigen Model-Objekte, ordnet
den View-Objekten die entsprechenden Model-Objekte zu und übergibt dann ein Array mit allen View-Objekten der View-Loop.
Sobald diese gestartet wird, aktualisiert sie regelmäßig die grafische Darstellung dieser Objekte im HTML-Dokument.
 
Die Init-Prozedur lädt ein weiteres Modul: „<code>control/keyboard</code>“. Diese fängt alle Tastaturereignisse ab.
Sobald auf der Tastatur eine entsprechende Taste gedrückt wird, ruft sie sie Methode „<code>start</code>“ des Schlägers
({{dh}} eines Objektes der Klasse „<code>ModelPaddle</code>“) auf und setzt ihn in Bewegung.
Welcher Taste welche Bewegungsrichtung zugeordnet ist, wird in der Init-Datei „<code>init.json</code>“ festgelegt.
 
Danach wird das eigentlich Spiel gestartet. Die Spiellogik wird in der Prozedur „<code>minipong</code>“ implementiert.
Diese Prozedur muss die Model-Objekte des Spiels kennen, da sie deren Werte liest und in bestimmten Situationen verändert.
Jede Änderung einen Model-Objekts, dem ein View-Objekt zugeordnet ist, wird fast sofort (genau gesagt, bei der nächsten Ausführung
der View-Loop) visualisiert. Da dies automatisch geschieht, muss die Prozedur „<code>minipong</code>“ nichts weiter machen,
als beispielsweise den Text innerhalb eines Text-Objekts der Klasse  „<code>ModelText</code>“ oder die Beschriftung des Buttons
innerhalb des Objekts der Klasse  „<code>ModelButton</code>“  zu ändern. Sobald dies geschehen ist, wird die Änderung im HTML-Dokument
automatisch übernommen.
 
Die Spiellogik hat zwei wichtige Aufgaben:
# Simulation der Ball/Schläger-Physik
# Reaktion auf Ereignisse wie Aktivierung des Start-Buttons (Spielstart), Kollision des Schlägers mit dem Ball (Punktgewinn), Verlust des Balles am unteren Bühnenrand (Spielende) etc.
 
[[Datei:MiniPong04 ClassModel ModelLoop.png|gerahmt|rechts|Klassendiagramm: Detailansicht der Model-Loop]]
Für die erste Aufgabe verwendet sie eine Model-Loop, die alle beweglichen Objekte, {{dh}} alle Model-Objekte, die über die Methode
„<code>move</code>“ verfügen, möglichst oft pro Sekunde mittels dieser Methode an eine neue Position verschiebt.
Das nebenstehende Diagramm zeigt abermals eine Detailansicht des Klassendiagramm, in dem alle für die ModelLoop notwendigen
Beziehungen eingetragen wurde. Auch hier gilt: Im Übersichtsdiagramm wurden die Beziehungspfeile, die im nebenstehenden Diagramm blau markiert wurden,
aus Gründen der Übersichtlichkeit  nicht eingetragen.
Die Model-Loop wird nicht von der Init-Prozedur, sondern von der Prozedur „<code>minipong</code>“ erzeugt.
Die Init-Prozedur benötigt dieses Objekt nicht. Die MiniPong-Prozedur benötigt es dagegen nicht nur, sondern muss es auch
gemäß ihren Bedürfnissen initialisieren: Der ModelLoop muss eine spezielle Kollisionsfunktion übergeben werden.
 
Die zweite Aufgabe „Reaktion auf Ereignisse“ zerfällt in zwei Aufgaben.
# Reaktion auf Aktionen des Benutzers.
# Reaktion auf Aktionen des Balls.
 
Für die Model-Loop gilt diesselbe Aussage wie für die View-Loop: Es ist gleichgültig, um welche Art von Model-Objekte es sich handelt.
Es muss nur sichergestellt sein, dass sie das [[Schnittstelle_(OOP)#Schnittstelle_.28Interface.29|Interface]] „<code>Movable</code>“ implementieren. Das heißt, sie müssen
eine Methode „<code>move(p_seconds)</code>“ bestzen. Model-Objekte, für die dies nicht gilt, können ihre Position nicht
kontinuierlich ändern und werden daher der Model-Loop auch nicht zur Behandlung übergeben.
 
In diesem Spiel gibt es nur eine Benutzeraktion, auf die <code>minipong</code> direkt reagieren muss: „Klick auf den Start-Stopp-Button“ .
Die zweite Aktion „Bewegung des Schlägers“ wird vom Keyboard-Controller direkt an den Schläger weitergeleitet.
Für die Aktion „Button-Klick“ muss <code>minipong</code> dem Button in jedem der beiden Spielzustände „Spiel gestoppt“ und „Spiel gestartet“
eine geeignete Callback-Funktion zuweisen, die die jeweils passende Aktion ausführt. Im Fall „Spiel gestoppt“ muss der Button-Klick das
Spiel starten und im Fall „Spiel gestartet“ muss er das Spiel beenden.
 
Der Ball kann zwei weitere Aktionen ausführen. Er kann mit dem Schläger kollidieren oder das Spielfeld verlassen. Beide Aktionen werden von
der Kollisionsbehandlung erkannt. Jedes mal, wenn eine wenn der Ball eine dieser beiden Aktionen durchführt
muss die Prozedur „<code>minipong</code>“ darüber informiert werden, damit sie entsprechend reagieren kann.
Hierzu definiert sie mit Hilfe der drei Hilfs-Prozeduren „<code>collisionBallPaddle</code>“,  „<code>collisionBallPaddle</code>“
und „<code>collisionStagePaddle</code>“ eine geeignete Kollissionsprozedur, die sie der Model-Loop zur Kollisionserkennung und
-behandlung übergibt.


==Neues Projekt anlegen==
==Neues Projekt anlegen==


Legen Sie ein neues Statisches Web-Projekt mit dem Namen <code>MiniPongCanvas03</code> an.
Legen Sie ein neues Projekt mit dem Namen <code>MiniPong04</code>an.


Speichern Sie dieses Projekt wie üblich in Ihrem Repository.
Erstellen Sie folgende Ordner:


==Dateien erstellen==
* <code>web</code>
* <code>web/css</code>
* <code>web/json</code>
* <code>web/js</code>
* <code>web/js/lib</code>
* <code>web/js/lib/require</code>
* <code>web/js/app</code>
* <code>web/js/app/collision</code>
* <code>web/js/app/control</code>
* <code>web/js/app/logic</code>
* <code>web/js/app/model</code>
* <code>web/js/app/view</code>


Kopieren Sie die Dateien <code>index.html</code> und <code>css/Main.css</code> von [[HTML5-Tutorium: Canvas: MiniPong 02|Teil 2]] des Tutoriums,
Kopieren Sie folgende Dateien des Projekts [https://glossar.hs-augsburg.de/beispiel/tutorium/es5/minipong/WK_MiniPong03/ MiniPong03]
passen Sie den Projekttitel in der Datei <code>index.html</code> an
in das neue Projekt:
und erstellen Sie die Dateien <code>js/CONSTANT.js</code> <code>js/Main.js</code> neu.


===<code>index.html</code>===
* <code>web/index.html</code> (Ersetzen Sie im Titel „<code>MiniPong03</code>“ durch „<code>MiniPong04</code>“ und ersetzen Sie im Copyright-Kommentar meinen Namen durch Ihren Namen.)
* <code>web/css/main.css</code>
* <code>web/js/lib/require/json.js</code>
* <code>web/js/lib/require/require.js</code>
* <code>web/js/lib/require/text.js</code>


Fügen Sie in den HTML-Rumpf (<code>body</code>) folgenden Code ''vor'' das <code>canvas</code>-Element
Fügen Sie in die Datei „<code>index.html</code>“ hinter dem Canvas-Element ein Button-Element ein:
ein:
<source lang="html5">
<form>
  <button id="button_start_stop" type="button"></button>
</form>
</source>


Erstellen Sie die Datei  „<code>web/js/main.js</code>“ und fügen Sie folgenden Code ein:
<source lang="javascript">
<source lang="javascript">
<p id="d_info">&nbsp;</p>
requirejs.config
</source>
({
  baseUrl: 'js', // By default load any modules from directory js
  paths :
  {
    app:      'app',


Der Text dieses Elements wird von JavaScript aus geändert, um den jeweiligen Punktestand anzuzeigen.
    model:    'app/model',
<code>&amp;nbsp;</code> ist ein nicht-trennbares Leerzeichen (Non-Breaking SPace) und sorgt dafür,  
    view:      'app/view',
dass sich Eclipse nicht über ein leeres <code>p</code>-Element beschwert.
    control:  'app/control',
    logic:    'app/logic',
    collision: 'app/collision',


Fügen Sie in den HTML-Rumpf (<code>body</code>) folgendes Formular ''hinter'' das <code>canvas</code>-Element
    loadjson:  'lib/require/json',
ein:
    text:      'lib/require/text',
    json:      '../json'
  }
});


<source lang="javascript">
requirejs
<form>
( ['loadjson!json/init.json', 'app/init'],
   <input id="d_start_stop" type="button" value=" "/>
   function(initJSON, init)
</form>
  {
    //init(window, initJSON);
  }
);
</source>
</source>


Dieser Button fungiert – je nach aktuellen Status des Spiels – als Start- oder Stopp-Button.
Für jedes Modulpaket wurden ein Ordner angelegt und eine Kurzbezeichnung des Modulpfades festgelegt:
Die Beschriftung sowie die Zuordnung der zugehörigen Aktion wird jeweils mittels JavaScript
an den aktuellen Zustand der Anwendung angepasst.


Das Attribut <code>value</code> muss laut HTML5-Standard vorhanden sein.
{| class="datatable-rt"
Dessen Wert dar nicht „leer“ sein. Daher wird es hier angegeben, obwohl
! Farbe !! Ordner !! Modulpfad !!  Modulzweck
es eigentlich unnötig ist, da der Wert sofort von JavaScript überschrieben wird.
|-
| data-title="Farbe"          | grau
| data-title="Ordner"        | <code>js/app</code>
| data-title="Modulpfad"    | <code>app</code>
| data-title="Modulzweck"  | Initialisierung der Anwendung
|-
| data-title="Farbe"          | blau
| data-title="Ordner"        | <code>web/js/app/model</code>
| data-title="Modulpfad"    | <code>model</code>
| data-title="Modulzweck"  | Model der Anwendung
|-
| data-title="Farbe"          | grün
| data-title="Ordner"        | <code>web/js/app/view</code>
| data-title="Modulpfad"    | <code>view</code>
| data-title="Modulzweck"  | Anwendungsview
|-
| data-title="Farbe"          | orange
| data-title="Ordner"        | <code>web/js/app/controller</code>
| data-title="Modulpfad"    | <code>controller</code>
| data-title="Modulzweck"  | Controller zur Behandlung der Benutzeraktionen
|-
| data-title="Farbe"          | flieder
| data-title="Ordner"        | <code>web/js/app/logic</code>
| data-title="Modulpfad"    | <code>logic</code>
| data-title="Modulzweck"  | Anwendungslogik
|-
| data-title="Farbe"          | gelb
| data-title="Ordner"        | <code>web/js/app/collision</code>
| data-title="Modulpfad"    | <code>collision</code>
| data-title="Modulzweck"  | Kollisionserkennung und -behandlung
|}


===<code>CONSTANT.js</code>===
Erstellen Sie für jedes Modul, das im Klassendiagramm aufgeführt ist (mit Ausnahme von <code>ModelStage</code>), eine '''leere''' Datei
in der dieses Modul implementiert wird. Achten Sie darauf, dass die Farbmarkierung der Module mit den Farbmarkierungen
der Modulordner übereinstimmt:


Fügen Sie alle Variablen der beiden Dateien <code>CONSTANT.js</code> von [[HTML5-Tutorium: Canvas: MiniPong 01#CONSTANT.js|Teil 1]]
'''<code>web/js/app</code>'''
und [[HTML5-Tutorium: Canvas: MiniPong 02#CONSTANT.js|Teil 2]] des Tutoriums in die Datei <code>CONSTANT.js</code> ds dritten Teils ein.
* <code>init.js</code>


'''Achtung: Definieren Sie keine Variable doppelt.'''
'''<code>web/js/app/model</code>'''
* <code>button.js</code>
* <code>ball.js</code>
* <code>paddle.js</code>
* <code>text.js</code>
* <code>loop.js</code>


Sie werde später noch weitere Konstanten in diese Datei einfügen,
'''<code>web/js/app/view</code>'''
wie z.B. Konstanten, die die Startposition von Ball und Schläger definieren,
* <code>button.js</code>
oder eine Konstate <code>PADDLE_FRICTION</code>, die die Reibung des Schlägers festlegt.
* <code>ball.js</code>
(Beachten Sie, dass in den Use Cases Folgendes gefordert wurde:
* <code>paddle.js</code>
''Wird der Schläger im Moment der Kollision bewegt, so wird der Ball abhängig von der Bewegungsrichtung und Geschwindigkeit des Schlägers abgelenkt.'')
* <code>text.js</code>
* <code>loop.js</code>


===<code>main.js</code>===
'''<code>web/js/app/control</code>'''
* <code>keyboard.js</code>


Fügen Sie die Variablen <code>g_ball</code>, <code>g_paddle</code>, <code>g_context</code>, <code>g_imterval</code>
'''<code>web/js/app/logic</code>'''
der beiden Dateien <code>main.js</code> von [[HTML5-Tutorium: Canvas: MiniPong 01#main.js|Teil 1]]
* <code>minipong.js</code>
und [[HTML5-Tutorium: Canvas: MiniPong 02#main.js|Teil 2]] des Tutoriums in die Datei <code>main.js</code> des dritten Teils ein.
Definieren Sie zusätzlich eine globale Variable <code>g_score</code>, in der der aktuelle Punktestand des Spiels abgelegt wird.


'''Achtung: Definieren Sie keine Variable doppelt.'''
'''<code>web/js/app/collision</code>'''
* <code>ball_paddle.js</code>
* <code>stage_ball.js</code>
* <code>stage_paddle.js</code>


====Das Ball-Objekt <code>g_ball</code>====
Fügen Sie in jede Datei ein RequireJS-Kommando ein, das dafür sorgt, dass die benötigten Module geladen und die darin definierten
Erweitern Sie das Ball-Objekt <code>g_ball</code> um eine Resetmethode, deren Aufgabe es ist, den Ball
Funktionen ({{dh}} Prozeduren bzw. Konstruktorfunktionen) dem Modul zur Verfügung gestellt werden. Welche Module
vor Spielbeginn zur Startposition zu verschieben. Außerdem muss diese Funktion eine Startgeschwindikeit
ein Modul benötigt, erkennt man an den roten Pfeilen im Klassendiagramm.
des Balls festlegen.


<source lang="javascript">
Von den meisten Module geht kein roter Pfeil aus. In diese können Sie jeweils folgenden Code einfügen:
// Moves the ball to its starting position
 
// and computes randomly a starting velocity vector.
<source lang ="javascript">
reset:
define
( [],
   function()
   function()
   { this.x  = BALL_X;
   { "use strict";
     this.y  = BALL_Y;
 
    this.vx =   (Math.random()<0.5?1:-1)
     return null;
              * (BALL_VX_MIN+Math.random()*(BALL_VX_MAX-BALL_VX_MIN));
   }
     this.vy =  (BALL_VY_MIN+Math.random()*(BALL_VY_MAX-BALL_VY_MIN));
);
   },
</source>
 
Von <code>minipong</code> gehen vier rote Pfeile aus. Das heißt, in die Datei „<code>minipong.js</code>“
müssen sie folgende RequireJS-Kommando einfügen:
 
<source lang ="javascript">
define
(['collision/ball_paddle', 'collision/stage_ball', 'collision/stage_paddle',
    'model/loop'
  ],
  function(collisionBallPaddle, collisionStageBall, collisionStagePaddle,
          ModelLoop
  )
  { "use strict";
 
     return null;
   }
);
</source>
</source>


Definieren Sie die Konstanten <code>BALL_X</code> und <code>BALL_Y</code> in der Datei <code>CONSTANT.js</code> so, dass  
Die Reihenfolge, in der Sie die benötigten Module laden, ist unwichtig.
der Ball in der Mitte des oberen Spielfeldrands zum liegen kommt. Legen Sie für die übrigen Konstanten
Wichtig ist nur, dass die Reihenfolge der Kurzbezeichnungen der Modulpfade mit
sinnvolle Werte fest.
der Reihenfolge der Parameter in der Callback-Funktion übereinstimmt.


Legen Sie im Ball-Objekt für alle [[Attribut]]e sinnvolle Initialwerte fest.
Das Init-Modul „<code>app/init</code>“ benötigt sehr viele andere Module, um seine Aufgabe zu erledigen.
Diejenigen Werte, die später von der <code>reset</code>-Methode gesetzt werden,
Entsprechend lang ist die Modulliste:
können einfach mit <code>0</code> oder <code>null</code> vorbelegt werden.


====Das Schläger-Objekt <code>g_paddle</code>====
<source lang ="javascript">
Erweitern Sie das Schläger-Objekt <code>g_paddle</code> um eine Resetmethode, deren Aufgabe es ist, den Schläger
define
auf die Startposition (die ebenfalls in der Datei <code>CONSTANT.js</code> definiert wird)
( ['model/button', 'view/button',
und die Startgeschwindigkeit des Schlägers (in x-Richtung) auf <code>0</code> zu setzen:
  'model/ball',  'view/ball',
  'model/paddle', 'view/paddle',
  'model/text',  'view/text',
  'view/loop',
  'control/keyboard',
  'logic/minipong'
  ],
  function(ModelButton, ViewButton,
          ModelBall,  ViewBall,
          ModelPaddle, ViewPaddle,
          ModelText,  ViewText,
          ViewLoop,
          controlKeyboard,
          minipong
          )
  { "use strict";


    return null;
  }
);
</source>
Jetzt fehlt noch die Datei „<code>web/json/init.json</code>“, die schon ziemliche Ausmaße angenommen hat,
weil sie für (fast) jedes Modul im Diagramm geeignete Initialwerte enthält:
<source lang="javascript">
<source lang="javascript">
// Moves the paddle to its starting postition
{
// and sets its velocity to zero.
  "canvas":
reset:  
  {
   function()
    "element": "canvas",
   { this.x = PADDLE_X;
    "width":   400,
     this.y  = PADDLE_Y;
    "height":  300
     this.vx = 0;
  },
 
  "game":
   {
    "fps": 120,
 
    "welcome":  "Willkommen bei MiniPong",
    "ballLost": "Das Spiel ist vorbei :-(",
 
     "startGame": "Spiel starten",
     "stopGame":  "Spiel beenden"
   },
   },
</source>


Legen Sie im Schläger-Objekt für alle [[Attribut]]e sinnvolle Initialwerte fest.
  "model":
Diejenigen Werte, die später von der <code>reset</code>-Methode festgelegt werden,
  {
können einfach mit <code>0</code> oder <code>null</code> vorbelegt werden.
    "buttonStartStop":
    {},


Definieren Sie für das Schläger-Objekt ein weiteres Attribut <code>friction</code> und
    "ball":
initialisieren Sie dieses mit der Konstanten <code>PADDLE_FRICTION</code>.
    {
Der Wert dieser Konstanten sollte ca. <code>0.3</code> betragen.
      "r":  10,
      "pos": { "x": 195 , "y": 10 },
      "vel": { "x": { "min":  50, "max": 200 },
              "y": { "min": 150, "max": 200 }
            }
    },


==Die Initfunktion==
    "paddle":
    {
      "width":    50,
      "height":    8,
      "pos":      {"x": 175, "y": 287},
      "vel":      {"x": 100, "y":  0},
      "acc":      {"x": 500, "y":  0},
      "friction": 0.3
    },


Die Funktion <code>f_init</code> unterscheidet sich nicht wesentlich von der
    "info":
Init-Funktion der beiden ersten Teile des Tutoriums.
    {
      "pos": {"x": 10, "y": 130}
    },


Neu ist lediglich der Aufruf der Observer-Funktion <code>o_stop_game</code> und das Fehlen
    "score":
der Timerdefinition. Der Animations-Timer wird im Spiel erst gestartet, wenn der Spieler
    {
den Start-Knopf gedrückt hat. Die Stopp-Funktion hat die Aufgabe,
      "pos":      {"x": 5, "y": 3},
das Spiel in die Ausgangssituation zu bringen (siehe Definition dieser Funktion am Ende des Artikels).
      "template": "Punkte: $1",
Sie wird im weiteren Spielverlauf jedesmal dann aufgerufen, wenn ein Spiel durch einen Spielfehler
      "value":    0
oder durch den Spieler beendet wird.
    }
  },


<source lang="javascript">
  "view":
// Execute the init function after the HTML page has been loaded.
  {
window.onload = f_init;
    "buttonStartStop":
    {
      "elementID": "button_start_stop"
    },
 
    "ball":
    {
      "color":      "#55AA55",
      "borderWidth": 1,
      "borderColor": "#000000"
    },
 
    "paddle":
    {
      "color":      "#aa55cc",
      "borderWidth": 1,
      "borderColor": "#000000"
    },


// Has to be called after the HTML page has been loaded.
    "info":
function f_init()
    {
{ var l_canvas = document.getElementById("d_canvas");
      "color":    "#000000",
      "font":      "bold 25px Verdana, Geneva, sans-serif",
      "textAlign": "center"
    },


  // Initialize the canvas.
    "score":
  l_canvas.width = CANVAS_WIDTH;
    {
   l_canvas.height = CANVAS_HEIGHT;
      "color": "#000000",
  g_context       = l_canvas.getContext("2d");
      "font":   "bold 20px \"Courier New\", Courier, monospace",
       "textBaseline": "top"
    }
  },


   // Reset the game (i.e. change its status to "waiting for game to be started").
   "control":
   o_stop_game();
   {
    "player":
    {
      "left":  {"key": "ArrowLeft",  "keyCode": 37},
      "right": {"key": "ArrowRight", "keyCode": 39}
    }
  }
}
}
</source>
</source>


Die Draw-Funktion <code>f_draw<code/> hat nur eine Aufgabe: Das Neuzeichnen der Bühne,
Wenn Sie alles richtig gemacht haben, sollten Sie jetzt die Datei  „<code>index.html</code>“ im Browser fehlerfrei laden können.
d.h. Das Löschen des Bühneninhalts und Das Neuzeichnen aller [[Sprites]]
Diese Datei hat noch keine Inhalte, mit Ausnahme eines leeren Canvas-Elements. In der Browser-Konsole sollten aber keine Fehler gemeldet werden.
auf der Bühne. Dazu ruft sie der Reihe nach die <code>draw</code>-Methoden der
einezelnen Sprites auf und übergibt diesen die Bühne auf der der jeweilige
Sprite gezeichnet werden soll. (In unseren Fall gibt es nur zwei Sprites: Ball und Schläger.)


Es ist allerdings ziemlich unbefriedigend, wenn man eine Web-Anwendung ausführt und überhaupt nichts zu sehen ist,
obwohl etwas passiert. Öffnen Sie in den Browser-Entwicklertools die Netzwerkansicht und laden Sie die HTML-Datei erneut. Dann sollten Sie sehen,
dass 23 Dateien geladen werden. (Hier ist später noch Optimierungsarbeit angesagt. Die Anzahl der Dateien muss reduziert und die Inhalte müssen komprimiert werden.
Das ist aber nicht Thema dieses Tutoriums.)
Sie können probehalber auch mal <code>console.log</code>-Befehle in die einzelnen Dateien einfügen, damit Sie sehen in welcher Reihenfolge die Module geladen werden.
<source lang ="javascript">
console.log('Modul "MODULNAME" wird geladen');
// Ersetzen Sie MODULNAME durch den Namen des aktuellen Moduls.
define
( [],
  function()
  { "use strict";
    console.log('Modul "MODULNAME" wurde geladen');
    // Ersetzen Sie MODULNAME durch den Namen des aktuellen Moduls.
    return null;
  }
);
</source>
Als Musterlösung gibt es das Projekt [https://glossar.hs-augsburg.de/beispiel/tutorium/es5/minipong/WK_MiniPong04/ WK_MiniPong04_empty (SVN)].
Die darin enthaltene Datei <code>[https://glossar.hs-augsburg.de/beispiel/tutorium/es5/minipong/WK_MiniPong04_empty/web/index.html index.html]</code>
verwendet allerdings den Befehl „<code>terminal.log</code>“, den Sie als Übungsaufgabe im Praktikum erstellen sollten. Damit werden die Log-Nachrichten
im HTML-Dokument und nicht in der Browser-Konsole ausgegeben. Außerdem wurde der Logging-Code etwas erweitert, sodass anhand von Einrückungen
zu sehen ist, welcher Code von welchem Modul geladen wird.
==Models und Views==
===Ball===
[[Datei:MiniPong04 ClassModel Ball.png|gerahmt|rechts|Ball-View und Ball-Model]]
In den vorangegangenen Teilen des Tutoriums war der Ball ständig sichtbar und ständig in Bewegung. Das heißt,
es wurden nur folgende Attribute und Methoden benötigt:
* <code>r</code> (Radius)
* <code>x</code> (aktuelle $x$-Position)
* <code>y</code> (aktuelle $y$-Position)
* <code>vx</code> (aktuelle Geschwindigkeit in $x$-Richtung)
* <code>vy</code> (aktuelle Geschwindigkeit in $y$-Richtung)
* <code>move(p_seconds)</code> (Bewegen des Balls an die neue Position, die sich aus der aktuellen Position, der Geschwindigkeit und der Anzahl der Sekunden seit der letzten Berechnung der Position ergibt)
Im eigentlichen Spiel werden jedoch viel mehr Attribute und Methoden benötigt. Der Ball ist beim Start der Web-Anwendung zunächst unsichtbar,
erst beim Spielstart wird er sichtbar gemacht
* <code>visible</code> (Flag, ob der Ball sichtbar oder unsichtbar ist)
* <code>show()</code> (Methode, um den Ball sichtbar zu machen)
* <code>hide()</code> (Methode, um den Ball unsichtbar zu machen)
Solange der Ball unsichtbar ist, bewegt er sich nicht (<code>vx === 0</code> und <code>vy === 0</code>) und befindet sich an seiner Startposition
(<code>x === x_start</code> und <code>y === y_start</code>). Bei einem Reset des Spiels werden Position und Geschwindigkeit auf passende Startwerte gesetzt.
Diese werden dem Konstruktor  „<code>ModelBall</code>“ im Parameter  „<code>p_init</code>“ übergeben und vom Konstruktor dann in folgenden Attribute gespeichert:
* <code>x_start</code> ($x$-Koordinate der Startposition)
* <code>y_start</code> ($y$-Koordinate der Startposition)
* <code>vx_start_min</code> (minimale Startgeschwindigkeit in $x$-Richtung)
* <code>vx_start_max</code> (maximale Startgeschwindigkeit in $x$-Richtung)
* <code>vy_start_min</code> (minimale Startgeschwindigkeit in $y$-Richtung)
* <code>vy_start_max</code> (maximale Startgeschwindigkeit in $y$-Richtung)
Man beachte, dass der Ball bei jedem Spielstart von derselben Position aus startet, aber dass die Startgeschwindigkeit (in Grenzen) zufällig gewählt wird.
Um das Spiel starten, beenden und neu starten zu können, werden drei weitere Methoden benötigt: 
* <code>reset()</code> (Ball anhalten, unsichtbar machen und auf die Startposition setzen)
* <code>start()</code> (Ball auf eine zufällige Startgeschwindigkeit setzen)
* <code>stop()</code> (Ball anhalten, {{dh}}, die Geschwindigkeit auf Null setzen)
Insgesamt ergibt sich damit folgender Code:
'''Konstruktor'''
<source lang="javascript">
<source lang="javascript">
// Clears the canvas and redraws all sprites (ball and paddle).
function ModelBall(p_init)
function f_draw()
{
{ g_context.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
  this.r            = p_init.r;


   g_ball.draw(g_context);
   this.x_start      = p_init.pos.x;
   g_paddle.draw(g_context);
  this.y_start      = p_init.pos.y;
  this.vx_start_min = p_init.vel.x.min;
  this.vx_start_max = p_init.vel.x.max;
  this.vy_start_min = p_init.vel.y.min;
  this.vy_start_max = p_init.vel.y.max;
 
   this.reset(); // initializes further attributes
}
}
</source>
</source>


Zusätzlich sollte man noch eine Funktion definieren, mit der man Informationen
'''Öffentliche Methoden'''
in das Textfeld der HTML-Seite schreiben kann:
<source lang="javascript">
ModelBall.prototype =
{
  reset:
    function()
    {
      this.stop(); // By default, the ball does not move around.
      this.hide(); // By default, the ball is invisible.
 
      this.x = this.x_start;
      this.y = this.y_start;
    },
 
  show:
    function() { this.visible = true; },
 
  hide:
    function() { this.visible = false; },
 
  stop:
    function()
    {
      this.vx = 0;
      this.vy = 0;
    },
 
  start:
    function()
    {
      // react only if the ball is not already moving
      if (this.visible === true && this.vx === 0 && this.vy === 0)
      {
        this.vx = (Math.random() < 0.5 ? 1 : -1)*
                  (this.vx_start_min +
                  Math.random()*(this.vx_start_max - this.vx_start_min)
                  );
        this.vy = (Math.random() < 0.5 ? 1 : -1)*
                  (this.vy_start_min +
                  Math.random()*(this.vy_start_max - this.vy_start_min)
                  );
      }
    },
 
  move:
    function(p_seconds)
    {
      this.x += this.vx * p_seconds;
      this.y += this.vy * p_seconds;
    }
};
</source>
 
Fügen Sie diesen Code in den Rumpf der Callback-Funktion in der Datei  „<code>model/ball.js</code>“ ein. Vergessen Sie nicht,
in dieser Callback-Funktionen den Return-Befehl
<source lang="javascript">
  return null;
</source>
durch den Return-Befehl
<source lang="javascript">
  return ModelBall;
</source>
zu ersetzen.


Die Ball-View ändert sich nur in einem Aspekt gegenüber der Version aus dem zweiten und dritten Teil des Tutoriums.
Die Draw-Funktion darf den Ball nur zeichnen, wenn er sichtbar ist. Kopieren Sie also den Inhalt der Datei
[https://glossar.hs-augsburg.de/beispiel/tutorium/es5/minipong/WK_MiniPong03/web/js/app/view/ball.js <code>view/ball.js</code>]
aus dem dritten Teil des Tutoriums in die neue Datei <code>view/ball.js</code> und fügen Sie die If-Anweisung
<source lang="javascript">
<source lang="javascript">
// Displays information on the HTML page.
if (this.model.visible === true)
function f_info(p_info)
{ document.getElementById("d_info").firstChild.nodeValue = p_info; }
</source>
</source>
vor den eigentlichen Zeichenbefehl „<code>p_context.drawImage</code>“ ein.
===Paddle===
[[Datei:MiniPong04 ClassModel Paddle.png|gerahmt|rechts|Schläger-Model und Schläger-View]]
Der Schläger ist sehr ähnlich aufgebaut wie der Ball. (Der Code ist also nicht [[DRY]]. Man sollte zwei allgemeine Klassen  „<code>ModelGeoObject</code>“
und „<code>ViewGeoObject</code>“ definieren, von denen alle Geo-Objekt-Klassen wie „<code>ModelBall</code>“, „<code>ViewBall</code>“ gemeinsame
Eigenschaften erben.)
Dem Schläger ist neben Position und Geschwindigkeit auch noch eine Beschleunigung zugeordnet.
Für alle drei Vektoren sind feste Startwerte vorgegeben, {{dh}} der Schläger befindet sich bei Spielbeginn stets an derselben Stelle,
startet, sobald der Benutzer ihn bewegt, mit derselben Geschwindigkeit und wird stets im gleichen Maße schneller, je länger der
Spieler die entsprechende Steuertaste gedrückt hält.
Gegenüber dem Ball gibt es ein weiteres Attribut, das für die Realisierung der Use Cases wichtig ist:
* <code>friction</code> (Reibung, genauer Reibungsfaktor)


==Observer==
Wenn sich der Schläger bei eine Kollision mit dem Ball bewegt, wird der Ball aufgrund der Reibung kurzzeitig vom Schläger in $x$-Richtung
[[Observer]] sind speziele Funktionen, die in Falle von betimmten [[Ereignis (OOP)|Ereignis]]sen (events),
mitgezogen. Das heißt, die $x$-Geschwindigkeit des Schlägers ändert sich. Diese wird berechnet, indem zur $x$-Geschwindigkeit
wie Timer-Event, Tasten-Event, Maus-Event etc. ausgeführt werden.
des Balls die $x$-Geschwindigkeit mal dem Reibungsfaktor des Schlägers addiert wird. Wenn die Reibung gleich Null ist hat also  bei einer Kollision
die Geschwindigkeit des Schlägers keine Auswirkung auf die Geschwindigkeit des Balls. Je größer der Reibungsfaktor gewählt wird, desto größer ist die Ablenkung.
In der Musterlösung wurde der Reibungsfaktor auf $0,3$ gesetzt.


Die beiden Observer <code>o_start_paddle_moving</code> und <code>o_stop_paddle_moving</code>
Zu guter Letzt werden noch vier berechnete Attribute für den Schläger definiert:
können direkt aus der Datei <code>main.js</code> von Teil zwei des Tutoriums übernommen werden.
Sie haben die Aufgabe, den Schläger in eine bestimmte Richtung zu bewegen, solange der Spieler
eine bestimmte Taste drückt.


Der Observer <code>o_redraw</code> wächst im Teil 3 des Tutoriums zum „Moloch“ an. Er hat die Aufgabe,
* <code>get left() { return this.x; }</code> (linke $x$-Koordinate des Schlägers)
eine Kollision zwischen Ball und Wand oder Ball und Schläger zu erkennen und entsprechend zu reagieren:
* <code>get right() { return this.x + this.width; }</code>  (rechte $x$-Koordinate des Schlägers)
* Ball und Wand (links, rechts, oben): Ball prallt ab
* <code>get top() { return this.y; }</code> (linke $y$-Koordinate des Schlägers)
* Ball und Wand (unten außerhalb des sichbaren Bereichs): Spielende
* <code>get bottom() { return this.y + this.height; }</code> (rechte $y$-Koordinate des Schlägers)
* Ball und Schläger: Ball prallt ab und wird evtl. abgelenkt; Erhöhung des Scores um einen Punkt
 
Außerdem muss diese Funktion auch noch den Canvas löschen und die neu positionierten
Diese Attribute vereinfachen die Kollisionsfunktionen etwas, bei denen mehrfach auf die verschiedenen Seiten des Schlägers zugegriffen werden muss.
Sprites zeichnen, sofern die Funktion nicht aufgrund des Spielendes vorzeitig verlassen wurde.
 
Insgesamt sieht die Implementierung des Schlägermodells folgendermaßen aus:
 
'''Konstruktor'''
<source lang="javascript">
<source lang="javascript">
//  An event observer:
function ModelPaddle(p_init)
//  It is called every 1000/FPS seconds, but only when the game has been started.
{
function o_redraw()  
   this.width    = p_init.width;
{ // Collision detection: ball <-> wall (left, right, top)
   this.height  = p_init.height;
  // Collision response:  change the moving direction of the ball
   if (g_ball.x <= g_ball.r || g_ball.x >= CANVAS_WIDTH - g_ball.r)
    g_ball.vx = -g_ball.vx;
   if (g_ball.y <= g_ball.r)
    g_ball.vy = -g_ball.vy;


   // Collision detection: ball <-> wall (bottom outside of the canvas)
   this.x_start = p_init.pos.x;
  // Collision response: stop the game (as the ball has vanished)
   this.y_start = p_init.pos.y;
  if (g_ball.y - 2*g_ball.r > CANVAS_HEIGHT)
  this.vx_start = p_init.vel.x;
  { o_stop_game();
  this.vy_start = p_init.vel.y;
    return; // leave o_redraw early
   this.ax_start = p_init.acc.x;
   }
  this.ay_start = p_init.acc.y;
   
  // Collision detection: ball <-> paddle
  // Collision response:  change the moving direction of the ball
  //                    + change the velocity of the ball
  //                      (due to the friction between the ball and the paddle)
  //                    + display the new score
  // Collision detection and handling has to be improved:
  // A collision with the small side of the paddle should be handled.
  if (  g_ball.y + g_ball.r    >= g_paddle.y  
      && g_ball.y + g_ball.r    <= g_paddle.y + g_paddle.height
      && g_ball.x + 0.5*g_ball.r >= g_paddle.x
      && g_ball.x - 0.5*g_ball.r <= g_paddle.x + g_paddle.width
    )
   { // Resolve penetration.
    if (g_ball.vy > 0)                // The ball is moving from top to bottom.
      g_ball.y = g_paddle.y - g_ball.r;
    else                              // The ball is moving from bottom to top.
      g_ball.y = g_paddle.y + g_paddle.height + g_ball.r;  


    g_ball.vy = -g_ball.vy;
  this.friction = p_init.friction;
    g_ball.vx += g_paddle.friction*g_paddle.vx;
   
    // Raise the score and display it.
    g_score++;
    f_info("Punkte: " + g_score);
  }
 
  // Move the ball.
  g_ball.move();
 
  // Move the paddle if possible.
  if (  (g_paddle.vx < 0 && g_paddle.x > 0)
      || (g_paddle.vx > 0 && g_paddle.x + g_paddle.width < CANVAS_WIDTH)
    )
    g_paddle.move();


   // Redraw the canvas.
   this.reset(); // initializes further attributes
  f_draw();
}
</source>
</source>


Im Spiel gibt es noch zwei weitere Observer. Der Observer
'''Öffentliche Methoden'''
<code>o_start_game</code> wird bei einem Klick auf den
<source lang="javascript">
Start/Stopp-Button aufgerufen, falls diese als Start-Button
ModelPaddle.prototype =
fungiert. Seine Aufgabe ist es, den Score auf Null zurückzusetzen,
{
den Start/Stopp-Button als Stopp-Button zu aktivieren, die
  reset:
Steuerung des Schlägers zu aktivieren, den Animations-Timer der  „physics engine“
    function()
zu starten und zu guter Letzt den eine Information über den
    {
Spielstart in das Textfeld auf der Bühne zu schreiben. Aktuell könnte
      this.stop(); // By default, the paddle does not move around.
im letzten Schritt auch gleich der aktuelle Punktestand (nnull Punkte)
      this.hide(); // By default, the paddle is invisible.
angezeigt werden.
 
      this.x = this.x_start;
      this.y = this.y_start;
    },
 
  show:
    function()
    { this.visible = true; },


<source lang="javascript">
  hide:
// An event observer:  
    function()
// It is called whenever a "start game event" is signalled.
    { this.visible = false; },
function o_start_game()  
 
{ // Reset the score.
  stop:
  g_score = 0;
    function()
    { this.vx = 0;
      this.vy = 0;
 
      this.ax = 0;
      this.ay = 0;
    },


   // Set the button "d_start_stop" to be a stop button (view and behaviour).
   start:
  document.getElementById("d_start_stop").value      = "Stopp";
    function(p_direction)
  document.getElementById("d_start_stop").onmousedown = o_stop_game;
    {
      // react only if the paddle is visible not already moving
      if (this.visible === true &&
          this.vx === 0 && this.vy === 0
        )
      {
        switch (p_direction)
        {
          case "left":
            this.vx = -this.vx_start;
            this.ax = -this.ax_start;
            break;
          case "right":
            this.vx =  this.vx_start;
            this.ax = this.ax_start;
            break;


  // React to key down and key up events.
          case "up":
  document.onkeydown = o_start_paddle_moving;
            this.vy = -this.vy_start;
  document.onkeyup  = o_stop_paddle_moving;
            this.ay = -this.ay_start;
            break;
          case "down":
            this.vy = this.vy_start;
            this.ay =  this.ay_start;
            break;
        }
      }
    },


   // Start the timer for redrawing the canvas every 1000/FPS seconds.
   move:
  g_timer = window.setInterval(o_redraw, 1000/FPS);
    function(p_seconds)
    { this.x  += this.vx * p_seconds;
      this.y  += this.vy * p_seconds;
      this.vx += this.ax * p_seconds;
      this.vy += this.ay * p_seconds;
    },


   // Show the current score (zero points)
   /** The left side of the paddle (read only). */
   f_info("Punkte: " + g_score);
   get left()   { return this.x; },
}
</source>


Beachten Sie, dass der Code <code>f_info("Punkte: " + g_score);</code>
  /** The right side of the paddle (read only). */
an zwei Stellen im Programm aufgerufen wird. Damit ist der Code nicht mehr
  get right()  { return this.x + this.width; },
[[Don't repeat yourself|DRY]]. Wenn die Anzeige der Punkte veränder werden soll,
muss dies an zwei Stellen erfolgen. Wie kann man den Code verbessern?


Die Aufgabe des Stopp-Spiel-Observers ist im Wesentlichen,
  /** The top side of the paddle (read only). */
die Aktionen des Start-Spiel-Servers rückgängig zu machen:
  get top()    { return this.y; },
* Der Animations-Timer wird gestoppt.
* Die Schläger-Steuerungstasten werden deaktiviert.
* Der Start-Stop-Button wird als Start-Button aktiviert.


Außerdem werden der Ball un der Schläger an ihre jeweiligen Startpositionen gesetzt.
  /** The bottom side of the paddle (read only). */
Anschließend muss die Bühne neu gezeichnet werden, damit die neuen Positionen dieser
  get bottom() { return this.y + this.height; }
biden Sprites auch sichtbar wird.
};
</source>


Beachten Sie, dass es hier empfehlenswert ist, das Prototyp-Objekt des Konstruktors nicht mit mehrere Befehle der Art
<source lang="javascript">
<source lang="javascript">
// An event observer:  
ModelPaddle.prototype.reset = function(){...};
// It is called whenever a "stop game event" is signalled.
ModelPaddle.prototype.show = function(){...};
function o_stop_game()
ModelPaddle.prototype.hide = function(){...};
{ // Stop the canvas redrawing timer.
...
   window.clearInterval(g_timer);
</source>
zu befüllen, sondern ein eigenes Prototyp-Objekt zu definieren:
<source lang="javascript">
ModelPaddle.prototype =
{
  reset: function(){...},
  show:  function(){...},
  hide:  function(){...},
  ...
   get left() { return this.x; },
  get right() { return this.x + this.width; },
  ...
};
</source>


  // Stop reacting to key down and key up events.
Der Grund ist, dass es keine einfache Syntax gibt, eine Getter- oder einer Setter-Methode zu einem
  document.onkeydown = null;
bestehenden Objekt hinzuzufügen. Im obigen Beispiel müsste man beispielsweise Folgendes schreiben,
  document.onkeyup  = null;
um Getter-Methoden zum Objekt „<code>ModelPaddle.prototype</code>“ nachträglich hinzuzufügen:


  //Set the button "d_start_stop" to be a start button (view and behaviour).
<source lang="javascript">
  document.getElementById("d_start_stop").value      = "Start";
Object.defineProperty(ModelPaddle.prototype,
  document.getElementById("d_start_stop").onmousedown = o_start_game;
                      'left',
                      { get: function() { return this.x; } }
                    );
Object.defineProperty(ModelPaddle.prototype,
                      'right',
                      { get: function() { return this.x + this.width; } }
                    );
...
</source>


  // Put the ball and the paddle on their starting positions
Die Paddle-View ändert sich wiederum nur in einem Aspekt gegenüber der Version aus dem zweiten und dritten Teil des Tutoriums.
  // and then redraw the canvas.
Die Draw-Funktion darf den Schläger nur zeichnen, wenn er sichtbar ist. Kopieren Sie also den Inhalt der Datei
  g_ball.reset();
[https://glossar.hs-augsburg.de/beispiel/tutorium/es5/minipong/WK_MiniPong03/web/js/app/view/paddle.js <code>view/paddle.js</code>]
  g_paddle.reset();
aus dem dritten Teil des Tutoriums in die neue Datei <code>view/paddle.js</code> und fügen Sie die If-Anweisung
  f_draw();
<source lang="javascript">
}
if (this.model.visible === true)
</source>
</source>
vor den eigentlichen Zeichenbefehl „<code>p_context.drawImage</code>“ ein.


Man beachte, dass die Befehle in umgekehrter Reihenfolge wie in der Start-Funktion
===Text===
ausgeführt werden. Dies wird bei komplementären Start-/Stop-Anweisungen üblicherweise so gemacht,
[[Datei:MiniPong04 ClassModel Text.png|gerahmt|rechts|Text-Model und Text-View]]
da es bei dieser Reihenfolge der Stop-Anweisungen i. Allg. nicht zu Abhängigkeitsfehlern kommt.
Wenn beispielsweise die Anweisung B nur dann funktioniert, wenn das Ergebnis der
Anweisung A vorliegt, dann muss A vor B gestartet aber nach B beendet werden.


Man beachte außerdem, dass die Stop-Funktion den Score nicht verändert, damit der Spieler
Im Spiel „MiniPong“ gibt es zwei Textfelder: Eines zum Anzeigen des Scores und eines zum Anzeigen von allgemeinen Informationen.
die erzielten Punkte auch nach Spielende noch lesen kann. Hier ist allerdings noch ein kleiner Fehler
Im Gegensatz zu Ball und Schläger bewegt sich ein Text (in diesem Spiel!) nicht. Daher benötigt man im Model wesentlich weniger Attribute und Methode.
enthalten.


==Anwendung testen und sichern==
* <code>x</code> ($x$-Position des Text-Aufhängepunkt)
Testen Sie Ihre Anwendung in gewohnter Weise.
* <code>y</code> ($y$-Position der Text-Baseline)
* <code>template</code> (ein optionaler String, der die Zeichenfolge „<code>$1</code>“ enthält)
* <code>value</code> (der Wert, der im Textfeld dargestellt werden soll)
* <code>text</code> (ein Read-only-Attribut:  „<code>return template.replace('$1', value)</code>“)
* <code>visible</code> (Sichtbarkeit des Textes)


Vergessen Sie nicht, sie im SVN-Repository zu sichern.
Das zugehörige View-Objekt legt das Aussehen des Textes fest:


=Erweiterung=
* <code>color</code> (Textfarbe)
* <code>font</code> (Textfont, analog zu CSS-Fonts)
* <code>textAlign</code> ([http://www.w3schools.com/tags/canvas_textalign.asp Aufhängepunkt]: <code>left</code>, <code>center</code>, <code>right</code>)
* <code>textBaseline</code> ([http://www.w3schools.com/tags/canvas_textbaseline.asp Baseline]: <code>top</code>, <code>bottom</code>, <code>middle</code>, <code>alphabetic</code>, <code>hanging</code>)


Erweitern Sie die Anwendung so, dass Sie zwei Schläger unabhängig voneinander
Damit sollte eigentlich klar sein, wie die Model- und die View-Klasse aussehen-
links und rechts am Bühnenrand bewegen können, d.h., dass zwei Spieler gegeneinander spielen können.
Fügen Sie diesen Code in den Rumpf der Callback-Funktion in der Datei  „<code>model/text.js</code>“ ein. Vergessen Sie nicht,
Jeder Spieler hat seinen eigenen Score. Der erste Spieler, der 7 Punkte erreicht, gewinnt.
in dieser Callback-Funktionen den Return-Befehl
<source lang="javascript">
  return null;
</source>
durch den Return-Befehl
<source lang="javascript">
  return ModelText;
</source>
zu ersetzen.


Am besten zeigen Sie die erzielten Punkte auch auf der Bühne selbst an (vgl. Bilder im [[Wikipedia:Pong|Wikipedia-Artikel zu Pong]]).
'''<code>ModelText</code>: Konstruktor'''
Wie man Text auf der Bühne darstellen kann, wurde im [[HTML5-Tutorium: Canvas: Hello World|Hello-World-Tutorium]] gezeigt.
<source lang="javascript">
function ModelText(p_init)
{
  this.x        = p_init.pos.x;
  this.y        = p_init.pos.y;


[[Medium:MiniPong03aCanvas.png|miniatur|ohne|945px|Das Objektmodell von MiniPong 03a]]
  this.template = p_init.template;
  this.value    = p_init.value;


Beachten Sie wieder das [[Programmierprinzip#Don.27t_repeat_yourself.2C_DRY.5B4.5D|Programmierprinzip  „Don't repeat yourself“ (Dry)]].
  this.visible  = true;
}
</source>
 
'''<code>ModelText</code>: Öffentliche Methoden'''
<source lang="javascript">
ModelText.prototype =
{
  // read only attribute
  get text()
  { return (this.value == null)
          ? ''
          : (this.template == null)
            ? this.value.toString()
            : this.template.replace('$1', this.value);
  }
};
</source>
 
Die View-Klasse ist auch nicht viel komplexer. Auf die Vorberechnung
eine Mini-Canvas, der anstelle des Textes in den Haupt-Canvas kopiert wird, wird hier verzichtet.
Da sich der Text-Inhalt ändern kann und üblicherweise auch regelmäßig ändert, müsste man
bei jeder Text-Änderung einen neuen derartigen Mini-Canvas erstellen. Das ist zwar möglich,
führt hier aber zu weit.
 
'''<code>ViewText</code>: Konstruktor'''
<source lang="javascript">
function ViewText(p_model, p_init /*, p_document*/)
{
  this.model        = p_model;
 
  this.color        = p_init.color        || 'black';
  this.font        = p_init.font        || 'normal';
  this.textAlign    = p_init.textAlign    || 'left';
  this.textBaseline = p_init.textBaseline || 'alphabetic';
}
</source>


'''Musterlösung''': <code>[http://glossar.hs-augsburg.de/beispiel/tutorium/html5_canvas/minipong/html5_canvas_minipong_03a/WebContent/index.html Minipong 3a]</code>
'''<code>ViewText</code>: Öffentliche Methoden'''
([http://glossar.hs-augsburg.de/beispiel/tutorium/html5_canvas/minipong/html5_canvas_minipong_03a SVN-Repository])
<source lang="javascript">
  ViewText.prototype.draw =
    function(p_context)
    {
      var l_model = this.model;


'''Siehe auch''': http://www.guimp.com/pong_flash.html; Hier ist der Name MiniPong wirklich gerechtfertigt.
      if (l_model.value == null || l_model.value.toString() === '')
        return;


=Probleme der Kollisionserkennung=
      // ALL font attributes must be set, as it cannot be
      // guaranteed that another module has changed some font
      // attributes before.
      p_context.font        = this.font;
      p_context.textAlign    = this.textAlign;
      p_context.textBaseline = this.textBaseline;
      p_context.fillStyle    = this.color;
      p_context.fillText(l_model.text,
                        (this.textAlign === 'center')
                          ? p_context.canvas.clientWidth/2
                          : l_model.x,
                        l_model.y
                        );
    };
</source>


Die in [[HTML5-Tutorium: Canvas: MiniPong 01#Probleme_der_Kollisionserkennung|Teil 1 des Tutoriums angesprochenen]] Probleme de  „Tunnelns“ und der „Überlappung“
'''Haben Sie an die Return-Anweisung „<code>return ViewText;</code>“ gedacht? '''
bei einer [[Kollisionserkennung und -behandlung#A-posteriori-Kollisionserkennung_.28auch_.E2.80.9Ediskrete_Kollisionserkennung.E2.80.9C.29|A-posteriori-Kollissionserkennung]] kann man in dieser Minipong-Version besonders schön veranschaulichen.


==Tunneln==
===Button===
Beispiel: [http://glossar.hs-augsburg.de/beispiel/tutorium/html5_canvas/minipong/html5_canvas_minipong_03_tunneling/WebContent/index.html modifizierte Minipong-Anwendung]
[[Datei:MiniPong04 ClassModel Button.png|gerahmt|rechts|Button-Model und Button-View]]


In diesem Beispiel wurden einfach die Framrate deutlich reduziert (auf 10 Frames pro Sekunde)
Einen grafischen Button, der im Canvas dargestellt wird, könnte man mit Hilfe eines Rechtecks oder Kreises und eines Textes realisieren.
sowie eine hohe Ballgeschwindigkeit (200 Pixel pro Sekunde in y-Richtung) eingestellt.  
Button-Klicks müsste man dann mit Hilfe eine Kollisionserkennung („Mausspitze kollidiert mit Button“) und -behandlung verarbeiten.


(Damit Sie den Effekt auf jeden Fall beobachten können, wurde außerdem die Steuerung des
Hier soll ein einfacherer Weg eingeschlafen werden: Als Button wird ein HTML-Button verwendet, der sich
Schlägers deaktiviert.)
außerhalb der Bühne befindet. Folgende Attribute sind in einem Model-Objekt enthalten:


==Überlappung==
* <code>label</code> (Der Text, mit dem der Button im HTML-Dokument beschriftet sein soll.)
Beispiele: [http://glossar.hs-augsburg.de/beispiel/tutorium/html5_canvas/minipong/html5_canvas_minipong_03_penetration/WebContent/index.html modifizierte Minipong-Anwendung],
* <code>class</code> (Ein CSS-Klassen-Name, der dem Button-Element im HTML-Dokument zugeordnet wird. Beispielsweise kann man eine CSS-Klasse „<code>.hidden</code>“ definieren, mit dem man den Button unsichtbar machen kann.)
[http://glossar.hs-augsburg.de/beispiel/tutorium/html5_canvas/minipong/html5_canvas_minipong_03a_penetration/WebContent/index.html modifizierte Pong-Anwendung],
* <code>onClick</code> (Eine Methode, die aufgerufen wird, sobald der Button geklickt wird.)


Um diesen Effekt zu erreichen wurde nur eine wesentliche Änderung am Source-Code vorgenommen.
Der Konstruktor übernimmt für diese Attribute wie üblich alle Initialwerte aus einen Init-Objekt namens „<code>p_init</code>“. Ein Problem besteht dabei allerdings.
In der Funktion <code>o_redraw</code> wurden folgende fünf Zeilen auskommentiert:
Das Init-Objekt wird üblicherweise aus einer JSON-Datei eingelesen. Eine derartige Datei kann keine JavaScript-Funktionen enthalten.
Das heißt, das Attribut „<code>onClick</code>“ wird üblicherweise mit „<code>null</code>“ initialisiert. Es ist dann die Aufgabe einer Logikkomponente
diesem Attribut zur rechten Zeit eine geeignete Prozedur zuzuweisen.


'''ModelButton: Konstruktor'''
<source lang="javascript">
<source lang="javascript">
// Resolve penetration.
function ModelButton(p_init)
if (g_ball.vy > 0)                 // The ball is moving from top to bottom.
{
   g_ball.y = g_paddle.y - g_ball.r;
   this.label  = p_init.label || null;
else                              // The ball is moving from bottom to top.
  this.class  = p_init.class || null;
   g_ball.y = g_paddle.y + g_paddle.height + g_ball.r;  
   this.onClick = p_init.onClick || null;
}
</source>
</source>


Diese Anweisungen verschieben den Ball im Falle einer Kollision soweit, dass er den Schläger nicht überlappt, sondern nur berührt.
Die View ist auch nicht sonderlich kompliziert.
Wenn diese Korrekturmaßnahme unterbleibt, kann der Ball in gewissen Situationen im Schläger hängen beleiben. Eine derartige Situation kann man beispielsweise erzwingen,
Der Konstruktor speichert wie üblich das zugehörige Model im Attribut „<code>model</code>“.
wenn man in der Datei <code>CONSTANT.js</code> der Minipong-Anwendung folgende Werte wählt:
Außerdem sucht er im HTML-Dokument denjenigen Button, der mit dem Model verknüpft werden soll.


'''ViewButton: Konstruktor'''
<source lang="javascript">
<source lang="javascript">
var BALL_VX_MIN  = 116; // pixels per second
function ViewButton(p_model, p_init, p_document)
var BALL_VX_MAX  = 116; // pixels per second
{
var BALL_VY_MIN  = 21; // pixels per second
  this.model  = p_model;
var BALL_VY_MAX  =  21; // pixels per second
  this.element = p_document.getElementById(p_init.elementID);
}
</source>
</source>


Aber auch ohne Modifikation der Startwerte des Balls kann dieser Effekt auftreten,  
Sie Draw-Methode macht nichts weiter, als alle Attribute, die im Model definiert sind,
wenn man den Ball während des Spiels geeignet mit dem Schläger ablenkt.
in das zugehörige HTML-Button-Element zu kopieren, sofern das jeweilige Attribut im Model
definiert ist und sich vom im HTML-Button-Element gespeicherten Wert unterscheidet.


=Weitere Verbesserungsmöglichkeiten=
'''ViewButton: Öffentliche Methoden'''
<source lang="javascript">
ViewButton.prototype.draw =
  function()
  {
    var l_model  = this.model,
        l_element = this.element;


Der hier vorgestellte Minipong-Code realisiert alle in der Use-Case-Beschreibung geforderten Anwendungsfälle:
    if (l_model.label != null && l_element.innerHTML !== this.model.label)
    { l_element.innerHTML = this.model.label; }
    if (l_model.class != null && l_element.className !== this.model.class)
    { l_element.className = this.model.class; }
    if (l_model.onClick != null && l_element.onclick !== this.model.onClick)
    { l_element.onclick = this.model.onClick; }
  };
</source>


[[Medium:MiniPongUseCases.png|miniatur|ohne|924px|Use Cases der Tutoriums-Anwendung <code>Minipong</code>]]
Da die Draw-Methode regelmäßig aufgerufen wird (ca. 60 mal pro Sekunde),
ändert sich das Aussehen und/oder das Verhalten des Button automatisch mit
jeder Änderung des Models.


Der Code ist dennoch nicht zufriedenstellend, da viele [[Programmierprinzipien]] verletzt werden.
Eigentlich ist das ein Overkill. So eine Änderung des Button-Labels und des Button-Verhaltens
Auf die Beachtung dieser Prinzipien könnte bei Mini-Anwendungen wie MiniPong sicherlich verzichtet werden,
passiert nur ein paar mal pro Spiel.
da der Aufwand größer ist als der Ertrag.
Hätte die Logik eine direkten Zugriff auf die View, könnte man es sich sparen,
Bei größeren Anwendungen sollte man dies Prinzipien allerdings tunlichst beachten, da man anderenfalls mit großer Wahrscheinlichkeit
den Button durch die View-Loop aktualisieren zu lassen.
keine robust laufende Anwendung und keinen wiederverwendbaren Code erhält.
Wenn man der Spiellogik – aus gutem Grund – keinen direkten Zugriff auf die View gewähren will
und man den Button auch nicht viele Dutzend mal pro Sekunde aktualisieren will, hilft der Einsatz des
so genannten [[Observer-Pattern]]s weiter. Dies soll hier aber nicht weiter verfolgt werden.


{{Codequalität
==Keyboard-Controller==
| application    = MiniPongCanvas03
[[Datei:MiniPong04 ClassModel controlKeyboard.png|gerahmt|rechts|Der Keyboard-Controller]]
| readability    = 4
| writability    = 3
| continuity      = 4
| customizability = 4
| dry            = 3
| demeter        = 5
| verifiability  = 2
| interfaces      = 6
| contract        = 0
| liskov          = 6
| modularity      = 1
}}


==Verstöße gegen das Prinzip der „[[Programmierprinzipien#Verst.C3.A4ndlichkeit.2C_Comprehesibility.2C_Lesbarkeit.2C_Readability|Verständlichkeit/Lesbarkeit]]“==
Der Controller zum Steuern des Paddles per Tastatur ändert sich nicht. Er kann eins zu eins
[https://glossar.hs-augsburg.de/beispiel/tutorium/es5/minipong/WK_MiniPong03/web/js/app/control/keyboard.js aus dem dritten Teil des Tutoriums übernommen]
und in die Datei <code>controller/keyboard.js</code> eingefügt werden.


Es gibt nur einen wesentlichen Verstoß gegen das Prinzip „Verständlichkeit/Lesbarkeit“:
==Kollisionserkennung und -behandlung==
* '''Die Kommentare wurden nicht im [[JSDoc]]-Format geschrieben.'''
[[Datei:MiniPong04 ClassModel Collision.png|gerahmt|rechts|Hilfsprozeduren für Kollisionserkennung und -behandlung]]
Dieses Format kann verwendet werden, um eine [[API]]-Dokumentation automatisch zu generieren.


Ansonsten wurde versucht, den Code so lesbar und verständlich wie möglich zu gestalten:
Im [[HTML5-Tutorium:_Canvas:_MiniPong_03|dritten Teil des Tutoriums]] wurde die Kollisionserkennung und -behandlung
als Teil des Models angesehen. Ihre einzige Aufgabe war es, Geschwindigkeit und Position von beweglichen Objekten
im Falle von Kollisionen zu korrigieren. Nun kommt noch eine weitere Aufgabe hinzu. Sie muss die Spiellogik über
Kollisionen informieren, die das Spielgeschehen beeinflussen. Im Fall von MiniPong sind das die Kollision von Schläger und Ball
(Punktgewinn) sowie die Kollision von Schläger und unterem Bühnenrand (Spielende).


* Die Breite des Codes wurde fast überall auch 80 Zeichen beschränkt, damit er auf normalem Dia-A4-Papier in normaler Schriftgröße ausgedruckt werden kann.
Bislang gab es lediglich ein Modul namens
* Der Code wurde sauber und platzsparend eingerückt.
„<code>[https://glossar.hs-augsburg.de/beispiel/tutorium/es5/minipong/WK_MiniPong03/web/js/app/model/collision.js|collision.js]<code>
* Klammerpaare werden leicht als solche erkannt.
zur Kollisionsbehandlung. Dieses Modul droht zum Giganten zu werden, wenn immer mehr und mehr Kollisionsarten behandelt werden müssen.
* Es gibt keine überflüssigen Leerzeilen.
Daher wird es in mehrere Teilmodule aufgespalten.
* Es gibt keine Tab-Symbole, damit der Code auch mit anderen Text-Programmen (wie z.B. in einem Browser) gut gelesen werden kann.
* Die Bezeichner von Variablen, Funktionen etc. unterscheiden sich syntaktisch so, dass man ihre Aufgabe sofort erkennt. Beispielsweise handelt es sich bei <code>g_ball</code> um eine globale Variable, bei <code>p_event</code> um einen [[Parameter]] und bei <code>CANVAS_WIDTH</code> um eine Konstante (vgl. [[Multimedia-Programmierung: Konventionen|Programmierkonventionen]]).
* Die Namen der Variablen, Parameter, Funktionen, Attribute, Methoden und Konstanten sind selbsterklärend. Beispielsweise wird in der Variablen <code>g_ball</code> ein Ballobjekt gespeichert.
* Der Code wurde mit sinnvollen Kommentaren versehen (für fortgeschrittene Programmierer sollte die Zahl allerdings etwas reduziert werden).


==Verstöße gegen das Prinzip der [[Programmierprinzipien#Schreibbarkeit.2C_Writability|Schreibbarkeit]]“==
Die Kollisionsprozedur „<code>collisionStagePaddle</code>“ kann eins zu eins vom dritten Teil des Tutoriums übernommen werden,
da die Kollision des Paddles mit der Wand keine Auswirkung auf das Spielgeschehen hat. Diese Kollision bewirkt lediglich,
dass der Schläger gestoppt wird. Und das erledigt die Prozedur von sich aus. Allerdings gibt es durchaus Situationen, in denen auch
eine Kollision zwischen Schläger und Wand von der Spiellogik behandelt werden muss. Beispielsweise kann man im Breakout-Spiel [[Wikipedia:Bolo|Bolo]]  
mit dem Schläger gegen die Wand „donnern“. Wenn man dies fest genug macht, wenn also der Schläger bei der Kollision mit der Wand eine gewisse
Geschwindigkeit hat, wird der Raum leicht erschüttert. Dies hat auch Auswirkungen auf den Ball, dessen Geschwindigkeitsvektor dadurch leicht verändert wird.
Auf diese Weise kann man den Ball, wenn er irgendwo feststeckt, häufig wieder frei bekommen. 


'''Die Entwicklungsumgebung ist in mancherlei Hinsicht nicht zufriedenstellend:'''
Die folgende Implementierung der Kollisionsprozedur „<code>collisionStagePaddle</code>“ unterscheidet sich in einer Hinsicht von der ursprünglichen
Implementierung. Anstatt die Ränder des Schlägers zu berechnen, werden die neuen Schläger-Attribute „<code>left</code>“, „<code>right</code>“, „<code>top</code>“ und „<code>bottom</code>“ verwendet. Fügen Sie diesen Code ins Modul „<code>collision/stage_paddle</code>“ ein (und vergessen Sie nicht, den Return-Befehl anzupassen).


* Juno ist deutlich träger als Indigo. Diese Erfahrung haben neben [[Wolfgang Kowarschick]] anscheinend mehrere Benutzer gemacht (siehe z.B. [https://bugs.eclipse.org/bugs/show_bug.cgi?id=385272], [http://www.eclipse.org/forums/index.php/m/901301/], [http://stackoverflow.com/questions/11639108/eclipse-juno-wtp-egit-dead-slow]).
<source lang="javascript">
* Juno ist anscheinend auch hinsichtlich der Fehlerkorrektur und der Autovervollständigung ein Rückschritt gegenüber Indigo (eigene Erfahrung von [[Wolfgang Kowarschick]]).
function collisionStagePaddle(p_stage, p_paddle)
{
  // If the paddle collides with the left wall of the stage,
  // stop it and move it back to the stage.
  if (p_paddle.vx < 0 && p_paddle.left <= 0)
  {
    p_paddle.stop();
    p_paddle.x = 0;
  }


Zur Lösung dieser Probleme sollte man versuchsweise auf Indigo umsteigen (auch wenn auf [http://www.eclipse.org/downloads/ http://www.eclipse.org/] Juno defaultmäßig zum Download angeboten wird).
  // If the paddle collides with the right wall of the stage,
Eine Alternative wäre evtl. auch die Entwicklungsumgebung [http://netbeans.org/ Netbeans] mit der ich ([[Wolfgang Kowarschick]]) sehr gute Erfahrungen hinsichtlich der Entwicklung Web-Anwendungen gemacht habe.
  // stop it and move it back to the stage.
  if (p_paddle.vx > 0 && p_paddle.right >= p_stage.width)
  {
    p_paddle.stop();
    p_paddle.x = p_stage.width - p_paddle.width;
  }


Aber selbst wenn man diejenige Entwicklungsumgebung gefunden hat, mit der man persönlich am Besten zurechtkommt, gibt es
  // If the paddle collides with the top wall of the stage,
weitere Verbesserungsmöglichkeiten:
  // stop it and move it back to the stage.
  if (p_paddle.vy < 0 && p_paddle.top <= 0)
  {
    p_paddle.stop();
    p_paddle.y = 0;
  }


* Für Code-Schnipsel, die man häufig verwendet, sollte man Tastaturkürzel definieren. Ein sehr interessantes Werkzeug für diesen Zweck ist [http://code.google.com/p/zen-coding/ Zen Coding] (siehe auch [http://nooshu.com/aptana-on-steroids-using-zen-coding Aptana on Steroids using Zen Coding] und [https://github.com/sergeche/eclipse-zencoding eclipse-zencoding]).
  // If the paddle collides with the bottom wall of the stage,
* Für Dateien (html, js, css ...) sollte man sich Templates anlegen bzw. die Templates der verwendeten Entwicklungsumgebung an seine Vorlieben anpassen.
  // stop it and move it back onto the stage.
* Das Grundgerüst des zu implementierenden Codes (Objekt-, Klassen-, Attribut-, Methoden-Definitionen etc.) sollte automatisch generiert werden (z.B. mit Hilfe eines UML-Werkzeugs).
  if (p_paddle.vy > 0 && p_paddle.bottom >= p_stage.height)
* Für Code und Modell gemeinsam sollte [[Refactoring]] verfügbar sein. Das heißt, es sollte jederzeit problemlos möglich sein, die Namen von Klassen, Objekten, Methoden, Attributen etc. konsistent in allen Dateien zu ändern.
  {
    p_paddle.stop();
    p_paddle.y = p_stage.height - p_paddle.height;
  }
}
</source>


==Verstöße gegen das Prinzip der „[[Programmierprinzipien#Stetigkeit.2C_Continuity|Stetigkeit]]“==
Die Kollisionsprozedur „<code>collisionStageBall</code>“ ist im Modul „<code>collision/stage_ball</code>“ enthalten.
Die Prozedur kann allerdings nicht eins zu eins vom dritten Teil des Tutoriums übernommen werden, da die Kollision mit
der unteren Wand anders behandelt werden muss, als die Kollision mit den übrigen Wänden.


Ein Programm-Code heißt [[Programmierprinzipien#Stetigkeit.2C_Continuity|stetig]],  
Bei einer Kollision mit der unteren Wand wird das Spiel beendet. Für die Behandlung des Spielendes ist die
wenn kleine Änderungswünsche auch nur kleine Änderungen am Code zur Folge haben.
Spiellogik zuständig. Um über diese Art der Kollision benachrichtigt zu werden, übergibt sie der
Kollisionsprozedur „<code>collisionStageBall</code>“ im Parameter „<code>cb_hit</code>“ eine Callback-Funktion,
die im Falle einer Kollision mit der unteren Wand aufgerufen werden soll. Damit die Spiel erst endet,
wenn der Ball die Bühne vollständig verlassen hat, wird die untere Wand etwas nach unten in den nicht sichtbaren Bereich
verschoben.


Dieses Prinzip wird relativ gut beachtet. Man vergleiche beispielsweise den Code
<source lang="javascript">
der MiniPong-Datei [http://glossar.hs-augsburg.de/beispiel/tutorium/html5_canvas/minipong/html5_canvas_minipong_03/WebContent/js/main.js <code>main.js</code>]
function collisionStageBall(p_stage, p_ball, cb_exit)
der Canvas-Version mit dem Code von [http://glossar.hs-augsburg.de/beispiel/tutorium/html_svg/minipong/html_svg_minipong_03/WebContent/js/main.js <code>main.js</code>]
{
der SVG-Version:
  // If the ball collides with the left or the right wall of the stage
# Beide Musterlösungen ([http://glossar.hs-augsburg.de/beispiel/tutorium/html5_canvas/minipong/html5_canvas_minipong_03/ Canvas-Version], [http://glossar.hs-augsburg.de/beispiel/tutorium/html_svg/minipong/html_svg_minipong_03/ SVG-Version]) in Eclipse laden
  // mirror its x-velocity and move the ball back onto the stage.
# Im Projektexplorer: beide <code>main.js</code>-Dateien selektieren
  if (p_ball.x <= p_ball.r)
# Rechtsklick auf eine der beiden Dateien → <code>Compare with</code> → <code>Einander</code>
  {
    p_ball.vx = -p_ball.vx;
    p_ball.x += 2*(p_ball.r - p_ball.x);
  }
  if (p_ball.x >= p_stage.width - p_ball.r)
  {
    p_ball.vx = -p_ball.vx;
    p_ball.x -=  2*(p_ball.r - p_stage.width + p_ball.x);
  }


Der Änderungswunsch lautete „Ersetze die Canvas-Bühne durch eine SVG-Bühne“. Der in zwei Tutorien
  // If the ball collides with the top wall of the stage
([[HTML5-Tutorium: Canvas: MiniPong 03]] und [[HTML-Tutorium: SVG: MiniPong 03]]) erstellte Code unterscheidet sich
  // mirror its y-velocity and move the ball back onto the stage.
nicht sonderlich stark.
  if (p_ball.y <= p_ball.r)
  {
    p_ball.vy = -p_ball.vy;
    p_ball.y += 2*(p_ball.r - p_ball.y);
  }


Hinsichtlich des Prinzips „Don't repeat yourself“ sind allerdings noch einige Verbesserungen möglich (siehe unten).
  // If the ball leaves the bottom of the stage, call cb_exit.
  // Factor 1.5: The ball is really outside the stage an thus invisible,
  // even if the view draws a very thick border around it.
  if (p_ball.y >= p_stage.height + 1.5*p_ball.r)
  { if (cb_exit)
      cb_exit();
  }
}
</source>


==Verstöße gegen das Prinzip der „[[Programmierprinzipien#Konfigurierbarkeit.2C_Customizability|Konfigurierbarkeit]]“==
Zu guter Letzt muss noch die Kollisionserkennung und -behandlung für Kollisionen des Balls mit dem Schläger
realisiert werden. Über jede derartige Kollision wird die Spiellogik ebenfalls mit einer Callback-Funktion informiert,
damit letztere den Punktestand aktualisieren kann.  


Ein wichtiges Prinzip, um Stetigkeit zu erzielen, ist, die einfache Konfigurirbarkeit der Anwendung sicherzustellen.
Die nachfolgende Implementierung der Kollisionserkennung und -behandlung ist ziemlich primitiv, da nur zwei Fälle
Konstanten sollten nie im Code stehen, sondern immer in einer eigenen Konfigurationsdatei.
unterschieden werden: Kollision des Balles mit der oberen oder der unteren Seite des Schlägers. Das reicht
zunächst für unsere Zwecke, führt aber schon zu Problemen, wenn ein senkrechter Schläger an einer Seitenwand
verwendet werden soll. In diesem Fall müsste die Kollisionsprozedur umgeschrieben werden.  


Gegen dieses Prinzip wird nur in einer Hinsicht verstoßen:
Bei einer korrekten Kollisionserkennung und -behandlung zwischen Kreis und Rechteck müssen 8 Fälle unterschieden werden:
Kollision des Balls mit einer der vier Seitenwände sowie Kollision des Balls mit einer der vier Ecken.
'''Texte, mit denen der Benutzer informiert wird (<code>Start</code>, <code>Stopp</code>, <code>Punkte</code>) stehen direkt im Code.'''


(Anmerkung: In der Datei [http://glossar.hs-augsburg.de/beispiel/tutorium/html5_canvas/minipong/html5_canvas_minipong_03a/WebContent/js/main.js <code>main.js</code>]
<source lang="javascript">
der zu [http://glossar.hs-augsburg.de/beispiel/tutorium/html5_canvas/minipong/html5_canvas_minipong_03a/ Pong erweiterten Version von MiniPong] sind noch deutlich mehr
function collisionBallPaddle(p_ball, p_paddle, cb_hit)
Informationstexte direkt im Code enthalten.)
{
  if (p_ball.y + p_ball.r    >= p_paddle.top    &&
      p_ball.y + p_ball.r    <= p_paddle.bottom &&
      p_ball.x + 0.5*p_ball.r >= p_paddle.left  &&
      p_ball.x - 0.5*p_ball.r <= p_paddle.right
    )
  {
    // Resolve penetration.
    if (p_ball.vy > 0)        // The ball is moving from top to bottom.
    { p_ball.y = p_paddle.top - p_ball.r; }
    else                      // The ball is moving from bottom to top.
    { p_ball.y = p_paddle.bottom + p_ball.r; }


Alle anderen Konstanten wurden in die Datei <code>CONSTANT.js</code> ausgelagert, die somit für MiniPong als Konfigurationsdatei fungiert.
    // Modify the velocity of the ball.
    p_ball.vy = -p_ball.vy;
    p_ball.vx += p_paddle.friction*p_paddle.vx;


In eine künftigen MiniPong-Version sollten die Informationstexte auch in eine Informationsdatei ausgelagert werden.
    // If the paddle hits the ball, invoke the callback function.
Im Rahmen dieser Auslagerung sollte MiniPong gleich „[[Internationalisierung|internationalisiert]]“ werden. Das heißt, die Informationstexte
    if (cb_hit)
sollten jeweils in der Sprache von Benutzer bevorzugten Sprache ausgeliefert werden.
    {  cb_hit(); }
  }
}
</source>


==Verstöße gegen das Prinzip „[[Don't repeat yourself]]“==
==Model- und View-Loop==
[[Datei:MiniPong04 ClassModel Loop.png|gerahmt|rechts|Model-Loop und View-Loop]]


Die Beactung des Prinzips „Don't repeat yourself“ (DRY) ist ein weiteres wichtiges Instrument,
Die Model-Loop sowie die View-Loop waren bislang Bestandteile des Moduls
um die Stetigkeit von Programmcode zu erhöhen.
„<code>[https://glossar.hs-augsburg.de/beispiel/tutorium/es5/minipong/WK_MiniPong03/web/js/app/minipong.js minipong.js]</code>“. Da dieses Modul deutlich größer wird,  
sollte es zerschlagen werden. Für die MiniPong-Anwedung wird es in vier Einzelmodule unterteilt:


'''Der Minipong-Code ist nicht [[Don't repeat yourself|DRY]].'''
* <code>init</code> (Erzeugung aller wesentlichen Objekte; Starten der View Loop; Verknüpfung des Keyboard-Controllers mit dem Paddle)
* <code>ViewLoop</code> (Visualisierung der aktuellen Zustände der grafischen Objekte des Spiels)
* <code>ModelLoop</code> (Berechnung der Positionen der beweglichen Objekte des Spiels; Information der Spiellogik über bestimmte Kollisionen)
* <code>minipong</code> (Die Spiellogik)


Beispiel: Der Code <code>f_info("Punkte: " + g_score);</code> wird
Zunächst werden die beiden Loops in eigenständige Module ausgelagert. Beide Module werden als Klassen realisiert.
an zwei Stellen im Programm aufgerufen. Wenn irgendwann einmal
Das heißt, es müssen jeweils ein <code>ViewLoop</code>- und ein  <code>ModelLoop</code>-Objekt erzeugt werden.
der Text der Punktausgabe verändert werden soll, muss dies an zwei Stellen erfolgen.
Diese Objekte kennen jeweils zwei Methoden <code>start</code> und <code>stop</code>, den denen die Loops
Ändert man den Text nur an einer Stelle, so verhält sich das Programm
gestartet und auch wieder angehalten werden können. Im Falle der View-Loop ist das für das Spiel MiniPong nicht
danach inkonsitstent.
sonderlich wichtig, da diese View nur einmal gestartet wird und dann dauerhaft aktiv ist. Bei einer komplexeren Web-Anwendung
solle man allerdings versuchen, die View-Loop nur während des eigentlichen Spiels zu aktivieren. So eine Loop
kostet Rechenpower und belastet damit insbesondere den Akku von mobilen Geräten.


Es gibt aber noch eine subtilere Form der Wiederholung von Code:
Die Model-Loop muss auch schon von der MiniPong-Spiellogik gestartet und gestoppt werden können. Dies wird insbesondere
Die grafischen Objekte (Ball, Schläger) enthalten
bei einer erweiterten Variante deutlich, bei dem das Spiel durch eine Pause-Taste temporär angehalten werden kann
gleiche Attribute (z.B. Position <code>x</code>, <code>y</code>) und
([https://glossar.hs-augsburg.de/beispiel/tutorium/es5/minipong/WK_MiniPong04/web/index_pause.html index_pause.html]).  
gleiche Methoden (<code>move</code>, <code>draw</code>) mit vergleichbaren Aufgaben.
Die Methode <code>move</code> könnte beispielsweise für alle grafischen
Objekte identisch definiert werden:


Die View-Loop erhält als Argumente das Fenster in dem die Web-Anwendung läuft, einen Canvas, auf dem grafische Objekte visualisiert werden sollen
sowie eine Liste, die die Views dieser Objekte enthält. Die View-Loop löscht zu Beginn den Canvas und ruft dann für alle View-Objekte die
Methode „<code>draw</code>“ auf. Dieser übergibt sie als Argument den 2D-Context des Canvas-Elements. Allerdings ist die Draw-Methode nicht
verpflichtet, ein Objekt auf den Canvas zu zeichnen. Sie kann auch ein Objekt im DOM-Baum des HTML-Dokuments aktualisieren.
Dies macht beispielsweise die Draw-Methode des Button-Objekts.
 
'''ViewLoop: Konstruktor'''
<source lang="javascript">
<source lang="javascript">
move:
function ViewLoop(p_window, p_canvas, p_views)
  function()
{
   { this.x  += this.vx/FPS;
   var l_context = p_canvas.getContext("2d"),
    this.y  += this.vy/FPS;
      n        = p_views.length;
    this.vx += this.ax * (this.vx > 0 ? 1 : -1);
 
    this.vy += this.ay * (this.vy > 0 ? 1 : -1);
  this.v_window = p_window;
  },
 
  this.m_update_view =
    function m_update_view()
    {
      // clear canvas
      l_context.clearRect(0, 0, p_canvas.width, p_canvas.height);
 
      // draw all visual objects
      for (var i = 0;  i <n; i++ )
      { p_views[i].draw(l_context); }
 
      p_window.requestAnimationFrame(m_update_view);
    };
}
</source>
</source>


Voraussetzung wäre allerdings, dass alle grafischen Objekte je einen Positionsvektor
Aktiviert und am Laufen gehalten wird die View-Loop wie üblich mit Hilfe der Methode „<code>window.requestAnimationFrame</code>“.
$p = (x,y)$, einen Geschwindigkeitsvektor $v = (v_x, v_y)$ sowie einen Beschleunigungsvektor $a = (a_x, a_y)$
Diese Methode liefert beim Aufruf eine Integerzahl zurück, die den Timer eindeutig identifiziert. Wenn man diesen Identifikator speichert,
besitzen würden.
kann man die rekursiven Aufrufe der Methode  „<code>window.cancelAnimationFrame</code>“ unterbrechen und so die Loop anhalten.
Dies wird ausgenutzt, um die Start- und die Stopp-Methode zu realisieren.


Selbst die obige Definition der Funktion <code>move</code> ist noch nicht DRY. Beispielsweise sollte
'''ViewLoop:Öffentliche Methoden'''
man an Stelle von
<source lang="javascript">
<source lang="javascript">
this.x += this.vx/FPS;
ViewLoop.prototype =
this.y += this.vy/FPS;
{
  start:
    function()
    {
      if (this.v_timer == null)
      { this.v_timer = this.v_window.requestAnimationFrame(this.m_update_view); }
    },
 
  stop:
    function()
    {
      if (this.v_timer != null)
      {
        this.v_window.cancelAnimationFrame(this.v_timer);
        delete this.v_timer;
      }
    }
};
</source>
</source>
besser mit echten 2D-Vektor-Objekten (wie z.B. <code>this.position</code> und <code>this.velocity</code>) arbeiten,
für die es Methoden wie <code>modifyAdd</code> und <code>scalar</code> gibt:


* <code>modifyAdd</code>: Modifiziert den aktuellen Vektor, indem sie einen zweiten Vektor hinzuaddiert.
Der Model-Loop-Konstruktor erwartet als Input eine Kollisionsprozedur, die
* <code>scalar</code>: Berechnet ein Skalarprodukt, d.h multipliziert den aktuellen Vektor mit einer reelen Zahl, modifiziert aber den aktuellen Vektor nicht.
(angestrebte) Update-Frequenz sowie eine Liste von Model-Objekten,
die bewegt werden sollen.
 
Der Konstruktor definiert ein „privates“ Attribut „<code>v_milliseconds</code>“ und eine „private“ Methode „<code>m_update_model</code>“
auf die die Start- und die Stop-Methode zugreifen können. (In Wirklichkeit handelt es sich um ein öffentliches Attribut und um eine öffentliche Methode,  
da im Prototype-Objekt keine privaten Elemente definiert werden können. Ich kennzeichne private Elemente einfach mittels eines Namenszusatzes
<code>v_...</code>“ – <code>v</code> für „(Zustands-)Variable“ – bzw. „<code>m_...</code>“ – <code>m</code> für „Methode“ – und weiß damit,
dass ich von außerhalb nicht auf derartige Elemente zugreifen darf.)


Die neue Position eines grafischen Objektes würde dann folgendermaßen ermittelt werden:
Die Methode „<code>m_update_model</code>“ ruft zunächst für alle Objekte die Move-Methode auf und führt anschließend (a posteriori)
mit Hilfe der Kollisionsprozedur eine Kollisionserkennung und -behandlung durch.


'''ModelLoop: Konstruktor'''
<source lang="javascript">
<source lang="javascript">
this.position.modifyAdd(this.velocity.scalar(1/FPS));
function ModelLoop(p_collision, p_f, p_models)
{
  var l_seconds = 1 / p_f;
  this.v_milliseconds = 1000 * l_seconds;
 
  this.m_update_model =
    function ()
    {
      // move around all movable objects
      for (var i = 0, n = p_models.length; i < n; i++)
      { p_models[i].move(l_seconds); }
 
      // detect and handle collision (a posteriori)
      p_collision();
    };
}
</source>
</source>


==Verstöße gegen das [[Programmierprinzipien#Gesetz_von_Demeter.5B5.5D.2C_Law_of_Demeter.2C_LoD|Gesetz von Demeter]]“==
Die Model-Update-Methode soll regelmäßig alle <code>v_milliseconds</code> Millisekunden aufgerufen werden.
Dazu wird die JavaScript-Funktion <code>setInterval</code>“ verwendet (die allerdings die Zeitvorgaben nicht sonderlich genau nimmt).
Diese Funktion liefert, wie auch schon <code>requestAnimationFrame</code>, beim Aufruf eine Integerzahl zurück,
mit der der Timer eindeutig identifiziert wird. Mit Hilfe dieses Identifikators und der Funktion  „<code>clearInterval</code>“
kann man den Timer  anhalten. Die Star- und die Stopp-Methoden können daher auf genau dieselbe Art realisiert werden,
wie bei der View-Loop:


Das [[Programmierprinzipien#Gesetz_von_Demeter.5B5.5D.2C_Law_of_Demeter.2C_LoD|Gesetz von Demeter]] verlangt, dass es keine Ketten von Methoden- und/oder
'''ModelLoop:Öffentliche Methoden'''
Attributaufrufen gibt, dass ein Objekt also nicht mit „Freunden von Freunden“ kommuniziert, sondern nur mit seinen direkten „Freunden“. Anweisungen der Art <code>object.attrtibut.methode()</code>, in der zwei oder mehr Punktoperationen ($\cdots$<code>.</code>$\cdots$)
<source lang="javascript">
vorkommen sind verboten. Auch dies erhöht die Stetigkeit.
ModelLoop.prototype =
{
  start:
    function()
    {
      if (this.v_timer == null)
      { this.v_timer = setInterval(this.m_update_model, this.v_milliseconds) }
    },


'''Gegen dieses Gesetz wird in <code>MiniPongCanvas03</code> nicht verstoßen.'''
  stop:
    function()
    {
      if (this.v_timer != null)
      {
        clearInterval(this.v_timer);
        delete this.v_timer;
      }
    }
};
</source>


In der SVG-Version [http://glossar.hs-augsburg.de/beispiel/tutorium/html_svg/minipong/html_svg_minipong_03/js/main.js <code>MiniPongSVG03</code>] kommt
==Initialisierung==
derartiger Code allerdings vor:
[[Datei:MiniPong04 ClassModel init.png|gerahmt|rechts|Initialisierung der Web-Anwendung]]


Die Initialisierungsprozedur „<code>init</code>“ hat zwei Aufgaben: Zunächst muss sie alle wesentlichen Objekte erstellen
und anschließend die Anwendung zu Laufen bringen.
Sie erwartet zwei Argumente: das Fenster, in dem die Anwendung läuft und das Initialisierungsobjekt,
das in der JSON-Datei definiert wurde:
<source lang="javascript">
<source lang="javascript">
p_canvas
function init(p_window, p_init)
  .circle(this.x, this.y, this.r)
  .attr({"stroke"      : BALL_BORDER_COLOR,
        "stroke-width" : BALL_BORDER_WIDTH,
        "fill"        : BALL_COLOR,
        });
</source>
</source>


In diesen speziellen Fall liegt allerdings kein Verstoß gegen das Gesetz von Demeter vor.
Anschließend muss sie die benötigten Objekte erstellen bzw. aus dem HTML-Dokument extrahieren.
 
Die View-Objekte benötigen das im  Browser-Fenster enthalten HTML-Dokument sowie das
darin enthalten Canvas-Element. Auch der Keyboard-Controller benötigt das HTML-Dokument.
 
<source lang="javascript">
var l_canvas_init = p_init.canvas,
    l_document    = p_window.document,
    l_canvas      = l_document.getElementById(l_canvas_init.element),
</source>
 
Als nächstes müssen alle Model- und View-Objekte erzeugt werden, mit Ausnahme des <code>ModelStage</code>-Objekts.
(Als  <code>ModelStage</code>-Objekt wird das Canvas-Init-Objekt verwendet. Es enthält die Größe und die Breite der Bühne,
und das ist alles was die Kollisionsprozedur von der Bühne wissen muss.)
 
Jedem Model-Objekt werden die Initialisierungsinformationen übergeben, die für das jeweilige Objekt im Initialisierungsobjekt
„<code>p_init</code>“ enthalten sind. Jedem View-Objekt werden nicht nur Initialisierungsinformationen aus dem
<code>p_init</code>-Objekt übergeben, sondern auch das Model, das es darstellen soll. Darüber hinaus benötigen
einige Objekte das Objekt „<code>p_dokument</code>“, um einen Mini-Canvas zum Cachen der grafischen Darstellung
des Models erstellen zu können.
 
<source lang="javascript">
l_model_button = new ModelButton(p_init.model.buttonStartStop),
l_view_button  = new ViewButton(l_model_button, p_init.view.buttonStartStop, l_document),
 
l_model_ball  = new ModelBall(p_init.model.ball),
l_view_ball    = new ViewBall(l_model_ball, p_init.view.ball, l_document),
 
l_model_paddle = new ModelPaddle(p_init.model.paddle),
l_view_paddle  = new ViewPaddle(l_model_paddle, p_init.view.paddle, l_document),
 
l_model_info  = new ModelText(p_init.model.info),
l_view_info    = new ViewText(l_model_info, p_init.view.info),


Bei <code>circle</code> und <code>attr</code> handelt es sich um so genannte [[Modifikationsmethode]]n.
l_model_score  = new ModelText(p_init.model.score),
Das sind Methoden, die den Zustand eines Objektes ändern, aber kein Ergebnis liefern sollen.
l_view_score  = new ViewText(l_model_score, p_init.view.score),
</source>


Für Modifikationsmethoden hat sich gerade in JavaScript eingebürgert, dass sie einfach das
Als nächstes werden die soeben erzeugten Model- und View-Objekte in Container gesteckt,  
von ihnen veränderte Objekt als Ergebnis zurückgeben, auch wenn dies eigentlich nicht notwendig wäre.
damit sie möglichst einfach an die Spiellogik bzw. die View-Loop übergeben werden können.
Der Vorteil dieses Vorgehens ist es, dass man auf ein und dasselbe Objekt
mehrere Modifikationsmethoden direkt nacheinander anwenden kann.  


Würden die Modifikationsmethoden nicht das von ihnen modifizierte Objekt zurückgeben,  
Die Modelle werden in ein [[Hasharray]] (= JavaScript-Objekt) gepackt, da die Spiellogik
wäre der Code etwas weniger stetig. Die Variable <code>p_canvas</code> müsste häufiger in den Code eingefügt werden:
namentlich auf die Objekte zugreifen können muss.
für die View-Loop wird ein einfaches Array als Container eingesetzt, da diese einfach der
Reihe nach für alle View-Objekte die Draw-Methode aufruft.


<source lang="javascript">
<source lang="javascript">
p_canvas
l_models = { stage:  l_canvas_init,
   .circle(this.x, this.y, this.r);
            button: l_model_button,
            ball:  l_model_ball,
            paddle: l_model_paddle,
            info:   l_model_info,
            score:  l_model_score
          },


p_canvas
l_views  = [ l_view_button, l_view_ball, l_view_paddle, l_view_info, l_view_score];
  .attr({"stroke"      : BALL_BORDER_COLOR,
        "stroke-width" : BALL_BORDER_WIDTH,
        "fill"        : BALL_COLOR,
        });
</source>
</source>


Das heißt, bei beiden der obigen Methodenaufrufen  „spricht“ das ROOT-Objekt nur, wie von Demeter gefordert,
Wie üblich muss noch die Größe des Canvas angepasst werden. Diese Größe ist wie immer im Objekt
mit dem ihm direkt bekannten „Freund“ <code>p_canvas</code>.
<code>l_canvas_init</code>“ enthalten.


==Verstöße gegen das Prinzip der „[[Programmierprinzipien#.C3.9Cberpr.C3.BCfbarkeit.2C_Verifiability|Überprüfbarkeit]]“==
<source lang="javascript">
l_canvas.width  = l_canvas_init.width;
l_canvas.height = l_canvas_init.height;
</source>


'''Bis auf ein einfaches Use-Cases-Diagramm und ein Objektmodell gibt es derzeit keine formale Spezifikation der Anwendung.'''
Zu guter Letzt muss Init-Prozedur eine View-Loop erzeugen und starten (das geht in einem Aufwasch,
die die Loop nicht mehr angehalten werden soll), den Keyboard-Controller dem Paddle zuweisen
und die Spiellogik starten.


Das heißt: Derzeit kann formal nicht beweisen werden, dass die Anwendung das tut, was von ihr erwartet wird.
Bei jedem dieser drei Aufrufe übergibt sie einige der zuvor erzeugten Objekte:
Es gibt auch keine Testfälle, mit denen die Erfüllung der Spezifikation zumindest teilweise verifiziert werden könnte.
Es bleibt dem Entwickler nur, Beta-Tester zu suchen, die das Spiel ausführlich auf diversen Brwosern testen und
ihm gefundene Probleme mitteilen. (Er selbst kann auch als Beta-Tester fungieren.)


Es fehlen:
* <code>ViewLoop</code>: alle View-Objekte
* <code>controlKeyboard</code>: das Paddle-Model
* <code>minipong</code>: alle Model-Objekte


* Ein gutes ([[UML]]-)[[Datenmodell]], das möglichst viele Aspekte der guten Programmierung umsetzt.
<source lang="javascript">
* Ein Code-Generator, der das Datenmodell automatisch in ein Codegerüst überführt.
new ViewLoop(p_window, l_canvas, l_views).start();
* Eine formale Spezifikation der Aufgaben der Funktionen und Methoden oder zumindest eine hinreichende Anzahl an Testfällen ([[unit test]]s), mit der die [[Semantik]] der Funktionen und Methoden beschrieben wird.
controlKeyboard(p_window, p_init.control.player, l_model_paddle);
* Ein Testwerkzeug, wie z.B. [http://busterjs.org Buster,js], um zu verifizieren, ob der aktuelle Code tatsächlich die mit Hilfe von Testfällen spezifizierte Semantik realisiert. (Siehe auch [[WikipediaEN:List_of_unit_testing_frameworks#JavaScript|Liste von Unit-Werkzeugen auf Wikipedia]])
minipong(p_init.game, l_models);
</source>


==Verstöße gegen das Prinzip „[[Programmierprinzipien#Benutze_Schnittstellen.2C_Make_Use_of_Interfaces|Benzutze Interfaces]]“==  
==MiniPong==
In JavaScript gibt es keine [[Klasse]]n und damit erst recht keine [[Schnittstelle_(OOP)|Interfaces]] (= Klasse ohne Implementierung der Methoden).
[[Datei:MiniPong04 ClassModel minipong.png|gerahmt|rechts|MiniPong: Die Spiellogik]]
Daher kann dieses Prinzip in JavaScript-Code höchstens auf Spezifikationsebene (d.h. in Objekt- und Klassendiagrammen),
aber nicht im Code selbst beachtet werden.


==Verstöße gegen das Prinzip [[Programmierprinzipien#Benutze_Integrit.C3.A4tsbedingungen.2C_Make_Use_of_Integrity_Constraints.2C_Design_by_Contract.5B2.5D|Design by Contract]]“==
Nachdem das Modul <code>logic/minipong</code>“ von allen übrigen Aufgaben befreit wurde, ist es nur noch für die Umsetzung der Spiellogik verantwortlich.


'''Da es für die MiniPong-Anwendung keinerlei formale Spezifikation gibt (zu der auch die Definition von Integritätsbedigungen zählt), verletzt diese Anwendung das Prinzip  „Design by Contract“ zu 100 Prozent.'''
Sie erhält von der Init-Prozedur die für sie bestimmten Initialisierungswerte sowie die Model-Objekte, die sie manipulieren kann und soll.
<source lang="javascript">
function minipong(p_init, p_models)
</source>


Das Prinzip „Design by Contract“ wurde von Bertrand Meyer formuliert.<ref name="Meyer">{{Quelle|Meyer, B. (1997): Object-oriented Software Construction}}</ref>
Zunächst speichert sie alle Model-Objekte in lokalen Variablen. Das hat den Vorteil, dass sie nicht immer
Es besagt im Wesentlichen, dass jede [[Operation]] ([[Funktion]], [[Prozedur]], [[Methode]] ...) einen „Vertrag“ zu erfüllen hat: Wenn der Aufrufer sicherstellt, dass die  
Aufrufe der Art „<code>p_models.xyz</code>“ tätigen muss. Außerdem legt sie ein leeres Array  „<code>l_models_movable</code>
so genannten [[Vorbedingung]]en erfüllt sind, führt die Operation ihre Aufgabe ordnungsgemäß aus. Das heißt, sie garantiert, dass durch die
an, das später alle beweglichen Model-Objekte enthält, {{dh}} alle Model-Objekte, für die die Methode „<code>l_move</code>“
Ausführung des zugehörigen Codes keine [[Invariante]]n verletzt werden und dass nach Abschluss der Operation alle [[Nachbedingung]]en erfüllt sind.
existiert. Zu guter Letzt legt sie in der Variablen „<code>l_model_loop</code>“ eine Model-Loop an, die die Position und Geschwindigkeit
der beweglichen Objekte regelmäßig aktualisiert. Als Kollisionsprozedur wird diesem Objekt die Funktion „<code>f_collision</code>“
übergeben. Diese Funktion wird weiter unten im Funktionsrumpf von <code>minipong</code> definiert.


Für jede Operation müssen also so genannte [[Integritätsbedingung]]en definiert werden:
<source lang="javascript">
var l_stage          = p_models.stage,
    l_button        = p_models.button,
    l_info          = p_models.info,
    l_score          = p_models.score,
    l_ball          = p_models.ball,
    l_paddle        = p_models.paddle,
    l_models_movable = [],
    l_model_loop    = new ModelLoop(f_collision, p_init.fps, l_models_movable);
</source>


* [[Vorbedingung]]en müssen erfüllt sein, bevor eine Operation ausgeführt werden kann.
Nun ist es an der Zeit, das  Array  „<code>l_models_movable</code>“ zu befüllen. Dazu wird einfach die Liste mit
* [[Nachbedingung]]en müssen erfüllt sein, nachdem eine Operation ausgeführt wurde.
allen Model-Objekt durchlaufen. Alle Objekte, die die Methode „<code>move</code>“ enthalten werden in dieses Array eingefügt.
* [[Invariante]]n müssten stets erfüllt sein.
<source lang="javascript">
// Store all model objects that have a move method within the array l_models_movable.
for (var k in p_models)
{ if (p_models.hasOwnProperty(k) && p_models[k].move != null)
  { l_models_movable.push(p_models[k]); }
}
</source>


Bei JavaScript gibt es noch nicht einmal die einfachste Form der Integritätsbedingungen, die [[Typisierung]] von Variablen und Parametern.
Mit den letzten beiden Anweisunges startet „<code>minipong</code>“ die Anwendung. Sie ruft
Jede Variable und jeder Parameter kann zu jedem Zeitpunkt einen [[Wert]] oder ein [[Objekt]] von einem anderen Typ haben.
dazu die Prozedur „<code>f_stop</code>“ auf (da der Benutzer das Spiel erst mittel eine Klicks auf den Start-Knopf starten muss) und schreibt eine
Es ist z.B. jederzeit möglich, der Variablen <code>x</code> des Ball-Objekts eine Zeichenkette als Wert zuzuweisen und
Willkommensbotschaft in Info-Textfeld (und überschreibt damit die Meldung, die die Stopp-Funktion ins Info-Textfeld geschrieben hat.).
damit einen Laufzeitfehler zu provozieren:


<source lang="javascript">
<source lang="javascript">
g_ball.x = "ganz links am Bildschirmrand";
// Stop the game and display a welcome message.
f_stop();
l_info.value = p_init.welcome;
</source>
</source>


Um dieses Problem abzumildern, muss man entweder
Nun müssen noch vier Prozeduren definiert werden, die in verschiedenen Spielsituationen aufgerufen werden.
Alle vier Prozeduren werden '''im Rumpf''' der Prozedur „<code>minipong</code>“ definiert. Das heißt, nur
<code>minipong</code> kann auf diese Prozeduren zugreifen. Sie kann sie allerdings als Callback-Funktionen
an andere Prozeduren und Methoden weiterleiten. Und genau das macht sie auch.


* geeignete Sicherheitsabfragen (z.B. mit Hilfe von Assertbefehlen wie <code>assert(isInteger(x))</code>) in den Code einfügen oder
Ganz wichtig für die Spiellogik ist die Definition einer geeigneten Kollisionsprozedur,
* zahlreiche zusätzliche Testfälle generieren, die in einer typisierten Sprache überflüssig wären, oder
die der Model-Loop übergeben wird, um die Kollisionserkennung und -behandlung damit durchzuführen.
* ein Werkzeug verwenden, dass die Korrektheit einer JavaScript-Anwendung automatisch beweist oder widerlegt.
Diese Prozedur verwendet die drei Hilfsprozeduren „<code>collisionBallPaddle</code>, „<code>collisionStageBall</code>“ und „<code> collisionStagePaddle</code>“,
die einfach nacheinander aufgerufen werden.


Die erste Lösung hat zwei gravierende Nachteile:
Zwei dieser Prozeduren erwarten als Input eine Callback-Funktion, die im Falle von bestimmten Kollisiones von der Kollisionsbehandlung aufgerufen werden.
Die Prozedur „<code>collisionBallPaddle</code>“ ruft die Callback-Funktion auf, sobald der Ball mit dem Schläger kollidiert.
* Fehler werden erst zur Laufzeit und nicht zur Entwicklungszeit (Compilezeit) entdeckt.
In diesem Fall soll der Punktestand erhöht werden. Das wird mit einer sehr einfachen anonymen Prozedur erledigt:
* Assert-Befehle verlangsamen den Code, während Integritätsbedingungen, die von einem Compiler zur Compilezeit berücksichtigt werden, den Code sogar beschleunigen können.
<source lang="javascript">
function(){ l_score.value++; }
</source>
Diese Prozedur erhöht bei jedem Aufruf im Text-Feld „<code>score</code>“ den Wert „<code>value</code>“ um eins.
Sobald ein neues Spiel gestartet wird, wird dieser Vert auf <code>0</code> gesetzt.


Die zweite Lösung ist machbar, aber ziemlich aufwändig, wenn man alle Testfälle „von Hand“ generieren muss.  
Die Prozedur „<code>collisionStageBall</code>“ informiert <code>minipong</code> mittels Callback, wenn der Ball die Bühne verlässt.
Als Callback-Funktion wird dieser Prozedur die Prozedur „<code>f_stop</code>“ (siehe unten) übergeben, um das Spiel zu beenden.


Die dritte Lösung wäre wunderbar geeignet, aber leider gibt es keine bislang keine derartigen Werkzeuge für JavaScript. Zumindest ist mir ([[Wolfgang Kowarschick]]) keines bekannt.
<source lang="javascript">
// Collision detection and handling.
function f_collision()
{
  collisionBallPaddle(l_ball, l_paddle, function(){ l_score.value++; });
  collisionStageBall(l_stage, l_ball, f_stop);
  collisionStagePaddle(l_stage, l_paddle);
}
</source>


Für die erste Lösung des Problems bietet sich eine Kompromisslösung an. Man versieht den Code mit Assertbefehlen und anderen Integritätstests
Ganz zu Beginn des Spiels, bei einem Abbruch durch den Spieler mittel Button-Klick und sobald der
und lässt diese später beim Komprimieren des JavaScript-Codes automatisch entfernen. So bekommt man zum Entwicklungszeitpunkt zumindest bei
Ball die Bühne verlässt wird das Spiel beendet. Dies ist die Aufgabe der Prozedur „<code>f_stop</code>“.
Laufzeittests aussagekräftige Fehlermeldungen, hat aber im Produktivbetrieb keine Leistungseinbußen.


==Verstöße gegen das [[Programmierprinzipien#Liskovsches_Substitutionsprinzip.5B3.5D.2C_LSP.2C_Ersetzbarkeitsprinzip.2C_Liskov_substitution_principle.5B4.5D|Liskovsches Substitutionsprinzip]]“==
Sie hält die Model-Loop und Ball an und macht Ball und Schläger unsichtbar.
Dann ändert sie das Aussehen und das Verhalten des Start-Stopp-Buttons:
Sie weißt ihm das Label <code>p_init.startGame</code>“ (<code>=== "Spiel starten"</code> gemäß <code>init.json</code>)
und die Prozedur „<code>p_start</code>“ zu. Das heißt, bei einem Klick auf diesen Button wird die Prozedur „<code>p_start</code>“
ausgeführt. Zu guter Letzt schreibt sie ins Info-Textfeld eine Nachricht, dass das Spiel beendet ist. Diese Nachricht
kann man überschreiben, indem man direkt im Anschluss an einen Aufruf von „<code>p_stop</code>“ eine
andere Nachricht ins Info-Textfeld schreibt. Dies geschieht direkt nachdem die Web-Anwendung gestartet wurde (siehe oben),


Das „Liskovsche Substitutionsprinzip“ besagt, dass man eine geerbte Methode nicht so überschreiben darf, dass sich
<source lang="javascript">
das Objekt überraschend anders verhält, als es es täte, wenn die Methode nicht überschrieben worden wäre.
// Stop the game.
function f_stop()
{
  l_model_loop.stop();
  l_ball.stop();
  l_ball.hide();
  l_paddle.hide();


Da Vererbung in MiniPong gar nicht verwendet wird, wird gegen dieses Prinzip auch nicht verstoßen.
  l_button.label  = p_init.startGame;
  l_button.onClick = f_start;


==Verstöße gegen das Prinzip der „[[Programmierprinzipien#Modularit.C3.A4t.2C_Modularity.2C_Teile_und_herrsche.2C_Divide_et_impera|Modularität]]“==
  l_info.value    = p_init.ballLost;
'''<code>MiniPongCanvas03</code> wurde als  „Moloch“ realisiert'''. Von Modularisierung ist allenfalls am Rande etwas zu merken:
}
</source>


* Die Konstanten wurden in eine eigene Datei ausgelagert (aber liegen immer noch direkt im ROOT-Objekt der Anwendung).
Wenn das Spiel angehalten wurde, kann man es mit einem Klick auf den Start-Stopp-Button starten.
* Die Ball- und Paddle-Methoden wurden in die das Ball- und das Schläger-Objekt integriert.
Die zugehörige Start-Prozedur muss das Spiel zunächst zurücksetzen. Sie setzt den Score-Wert auf <code>0</code>,
löscht den Inhalt des Info-Textfeldes und setzt Schläger und Ball zurück an die Startpositionen.
Dann ändert sie das Aussehen und das Verhalten des Start-Stopp-Buttons:
Sie weißt ihm das Label „<code>p_init.stopGame</code>“ (<code>=== "Spiel beenden"</code> gemäß <code>init.json</code>)
und die Prozedur „<code>p_stop</code>“ zu. Das heißt, bei einem Klick auf diesen Button wird die Prozedur „<code>p_stop</code>“
ausgeführt und das Spiel sofort beendet.


Hier besteht daher noch großes Verbesserungspotenzial. Es gibt immer noch viele zu viele globale Variablen (<code>g_...</code>) und zu viele globale Funktionen
Nun kann sie das eigentliche Spiel starten. Dazu macht sie Schläger und Ball sichtbar und startet dann den Ball und die Model-Loop.
(<code>f_...</code> sowie alle Observer-Funktionen). Bei sauberer Modularisierung des Codes lösen sich viele der zuvor beschriebenen
Jetzt sind die Call-Backfunktionen der Kollisionsprozedur scharfgeschaltet. Das heißt, Kollisionen vom Schläger mit dem Ball
Probleme in Wohlgefallen auf.
werden mit einem Puktgewinn belohnt (dieser wird durch die View-Loop auch sofort angezeigt).
Wenn der Ball die Bühne verlässt oder wenn der Benutzer den Start-Stopp-Button drückt, wird das Spiel beendet.
Der erspielte Score ist zu diesem Zeitpunkt noch sichtbar. Erst mit einem neuen Spielstart wird er wieder auf <code>0</code>
gesetzt.
 
<source lang="javascript">
// Start the game.
function f_start()
{
  l_score.value = 0;
  l_info.value  = '';
  l_ball.reset();
  l_paddle.reset();
 
  l_button.label  = p_init.stopGame;
  l_button.onClick = f_stop;
 
  l_paddle.show();
  l_ball.show();
  l_ball.start();
  l_model_loop.start();
}
</source>


'''Anmerkung:''' Verglichen mit einer früheren Version von <code>MiniPongCanvas03</code> ist der aktuelle Code allerdings schon richtig modular.
==Main==
Man sehe sich mal
[http://glossar.hs-augsburg.de/beispiel/tutorium/html5_canvas/minipong/html5_canvas_minipong_03_moloch/WebContent/js/main.js <code>main.js</code>]
der [http://glossar.hs-augsburg.de/beispiel/tutorium/html5_canvas/minipong/html5_canvas_minipong_03_moloch/ Vorgänger-Version vom Wintersemester 2011] an.
Hier ist sämlicher Code in einer Datei enthalten (die Konstanten sind im Code verteilt), ein Ball-Objekt und ein Schläger-Objekt gibt es auch noch nicht.


=Quellen=
Jetzt müssen Sie in der Datei „<code>main</code>“ noch den Kommentar vor dem Aufruf der Prozedur „<code>init</code>“ löschen.
<references/>
<ol start = "2">
<li>{{Quelle|Braun, H. (2011): Webanimationen mit Canvas}}</li>
<li>{{Quelle|Kowarschick, W.: Multimedia-Programmierung}}</li>
</ol>


=Fortsetzung des Tutoriums=
Damit wurden alle Module vollständig erstellt und MiniPong sollte gespielt werden können.


Sie sollten nun das [[HTML-Tutorium: SVG: Hello World|Hello-World-SVG-Tutorium]] bearbeiten, sofern Sie dies noch nicht gemacht haben.
==Quellen==
Anderenfalls können Sie gleich mit dem [[HTML-Tutorium: SVG: MiniPong|Minipong-SVG-Tutorium]] forfahren.
# {{Quelle|Braun, H. (2011): Webanimationen mit Canvas}}
# {{Quelle|Kowarschick, W.: Multimedia-Programmierung}}
[[Kategorie: HTML5-Tutorium: Canvas: MiniPong]][[Kategorie: HTML5-Beispiel]][[Kategorie:Kapitel:Multimedia-Programmierung:Beispiele]]
[[Kategorie: HTML5-Tutorium: Canvas: MiniPong]][[Kategorie: HTML5-Beispiel]][[Kategorie:Kapitel:Multimedia-Programmierung:Beispiele]]

Aktuelle Version vom 1. März 2023, 13:18 Uhr

Dieser Artikel erfüllt die GlossarWiki-Qualitätsanforderungen nur teilweise:

Korrektheit: 3
(zu größeren Teilen überprüft)
Umfang: 3
(einige wichtige Fakten fehlen)
Quellenangaben: 5
(vollständig vorhanden)
Quellenarten: 5
(ausgezeichnet)
Konformität: 5
(ausgezeichnet)

HTML-Tutorium: MiniPong

MiniPong: | Teil 1 | Teil 2 | Teil 3 | Teil 4 | Teil 5

Musterlösung: index.html (WK_MiniPong04 (SVN))

Leeres Projekt: index.html (WK_MiniPong04_empty (SVN))

Ziel: Das fertige Spiel „MiniPong“

Im vierten Teil des Tutoriums wird eine funktionsfähige Version von MiniPong erstellt.

Use Cases

Use Cases der Tutoriums-Anwendung MiniPong
Use Cases der Tutoriums-Anwendung MiniPong

Ein Spieler kann das Spiel starten (Startknopf) und vorzeitig beenden (Stopp-Knopf). Nach Spielstart kann der Spieler den Schläger am unteren Rand des Spielfeldes mit Hilfe der Richtungstasten des Keyboards nach links und rechts bewegen.

Nachdem das Spiel gestartet wurde, bewegt sich der Ball geradlinig im Spielfeld, wobei die Startrichtung zufällig gewählt wird. Kollisionen mit der linken, oberen oder rechten Wand haben eine Richtungsänderung des Balls zur Folge (Einfallswinkel = Ausfallwinkel). Eine Kollision mit der unteren Wand beendet das Spiel.

Ziel des Spiels sind möglichst viele Kollision des Balls mit dem Schläger. Eine derartige Kollision hat eine Richtungsänderung sowie einen Punktgewinn zur Folge. Der aktuelle Punktestand (Score) wird der Benutzer jederzeit angezeigt.

Wird der Schläger im Moment der Kollision bewegt, so wird der Ball abhängig von der Bewegungsrichtung und Geschwindigkeit des Schlägers abgelenkt (Simulation von Reibung).

Klassenmodell

Klassendiagramm

Eine genauere Analyse der Use Cases zeigt, welche Module benötigt werden. Die verschiedenen Module sind verschieden eingefärbt:

  • grau: Initialisierung der Anwendung
  • blau: Model der Anwendung
  • orange: Controller zur Behandlung der Benutzeraktionen (mit Ausnahme von Formularelementen, die in der View enthalten sind)
  • flieder: Anwendungslogik
  • grün: Anwendungsview
  • gelb: Kollisionserkennung und -behandlung

Die Kollisionserkennung und -behandlung erhält eine eigene Farbe, da sie eine Mischung aus Model und Controller ist. Einerseits verändert sie die Bewegungseigenschaften beweglicher Objekte (Ball und Schläger). Das heißt, sie ist wesentlicher Bestandteil des Models. Andererseits informiert sie die Anwendung über Aktionen des Balls, damit diese entsprechend reagieren kann. Dies ist eine typische Controllertätigkeit.

Das Modul „init“ hat die Aufgabe das Spiel zu initialisieren. Es definiert eine Prozedurinit“, die zunächst die wesentlichen Objekte erstellt und dann das Spiel startet. Im Klassendiagramm sind rote Pfeile zu den Modulen eingezeichnet, die das Init-Modul benötigt. Das sind die beiden Funktionsmodule „logic/minipong“ und „control/keyboard“ sowie diverse Model- und View-Klassen. Für jede dieser Klassen – mit Ausnahme der Klasse „ModelStage“ – erzeugt init ein oder im Falle der Text-Klassen sogar jeweils zwei Objekte. Als Stage-Objekt kommt – wie im dritten Teil des Tutoriums – das in der HTML-Datei enthaltene Canvas-Element zum Einsatz.

Die View-Objekte sind ausschließlich der Prozedur „init“ bekannt. Diese Objekte visualisieren die Model-Objekte, d. h. den aktuellen Zustand des Spiels. Da sich der Zustand des Spiel häufig ändert, erstellt init eine View-Loop, deren Aufgabe es ist, die Visualisierung möglichst häufig, am Besten 60 mal pro Sekunde zu erneuern. Dazu ruft die View-Loop, sobald sie einmal gestartet wurde, regelmäßig die Zeichenmethode „draw“ der einzelnen View-Objekte auf.

Klassendiagramm: Detailansicht der View-Loop

Die View umfasst sechs Elemente:

  • einen Button (Start, Stopp)
  • einen Ball
  • einen Schläger
  • zwei Textfelder (Score und allgemeine Informationen)

Die View-Loop muss diese sechs Objekte selbstverständlich kennen, damit sie sie darstellen kann. Allerdings reicht es, wenn ihr eine Liste von View-Objekten übergeben wird. Die einzige Forderung, die die die View-Loop an diese Objekte stellt, ist, dass für sie eine Zeichenmethode „draw(p_context)“ defiert wurde, mittels derer sie die Objekte auf dem Canvas (oder evtl. auch im HTML-Dokument selbst) visualisieren kann. Das heißt, die View-Klassen, deren Objekte von der View-Loop visualisiert werden sollen, müssen das InterfaceView“ implementieren.

Die Init-Prozedur erzeugt die sechs View-Objekte sowie die zugehörigen Model-Objekte, ordnet den View-Objekten die entsprechenden Model-Objekte zu und übergibt dann ein Array mit allen View-Objekten der View-Loop. Sobald diese gestartet wird, aktualisiert sie regelmäßig die grafische Darstellung dieser Objekte im HTML-Dokument.

Die Init-Prozedur lädt ein weiteres Modul: „control/keyboard“. Diese fängt alle Tastaturereignisse ab. Sobald auf der Tastatur eine entsprechende Taste gedrückt wird, ruft sie sie Methode „start“ des Schlägers (d. h. eines Objektes der Klasse „ModelPaddle“) auf und setzt ihn in Bewegung. Welcher Taste welche Bewegungsrichtung zugeordnet ist, wird in der Init-Datei „init.json“ festgelegt.

Danach wird das eigentlich Spiel gestartet. Die Spiellogik wird in der Prozedur „minipong“ implementiert. Diese Prozedur muss die Model-Objekte des Spiels kennen, da sie deren Werte liest und in bestimmten Situationen verändert. Jede Änderung einen Model-Objekts, dem ein View-Objekt zugeordnet ist, wird fast sofort (genau gesagt, bei der nächsten Ausführung der View-Loop) visualisiert. Da dies automatisch geschieht, muss die Prozedur „minipong“ nichts weiter machen, als beispielsweise den Text innerhalb eines Text-Objekts der Klasse „ModelText“ oder die Beschriftung des Buttons innerhalb des Objekts der Klasse „ModelButton“ zu ändern. Sobald dies geschehen ist, wird die Änderung im HTML-Dokument automatisch übernommen.

Die Spiellogik hat zwei wichtige Aufgaben:

  1. Simulation der Ball/Schläger-Physik
  2. Reaktion auf Ereignisse wie Aktivierung des Start-Buttons (Spielstart), Kollision des Schlägers mit dem Ball (Punktgewinn), Verlust des Balles am unteren Bühnenrand (Spielende) etc.
Klassendiagramm: Detailansicht der Model-Loop

Für die erste Aufgabe verwendet sie eine Model-Loop, die alle beweglichen Objekte, d. h. alle Model-Objekte, die über die Methode „move“ verfügen, möglichst oft pro Sekunde mittels dieser Methode an eine neue Position verschiebt. Das nebenstehende Diagramm zeigt abermals eine Detailansicht des Klassendiagramm, in dem alle für die ModelLoop notwendigen Beziehungen eingetragen wurde. Auch hier gilt: Im Übersichtsdiagramm wurden die Beziehungspfeile, die im nebenstehenden Diagramm blau markiert wurden, aus Gründen der Übersichtlichkeit nicht eingetragen.

Die Model-Loop wird nicht von der Init-Prozedur, sondern von der Prozedur „minipong“ erzeugt. Die Init-Prozedur benötigt dieses Objekt nicht. Die MiniPong-Prozedur benötigt es dagegen nicht nur, sondern muss es auch gemäß ihren Bedürfnissen initialisieren: Der ModelLoop muss eine spezielle Kollisionsfunktion übergeben werden.

Die zweite Aufgabe „Reaktion auf Ereignisse“ zerfällt in zwei Aufgaben.

  1. Reaktion auf Aktionen des Benutzers.
  2. Reaktion auf Aktionen des Balls.

Für die Model-Loop gilt diesselbe Aussage wie für die View-Loop: Es ist gleichgültig, um welche Art von Model-Objekte es sich handelt. Es muss nur sichergestellt sein, dass sie das InterfaceMovable“ implementieren. Das heißt, sie müssen eine Methode „move(p_seconds)“ bestzen. Model-Objekte, für die dies nicht gilt, können ihre Position nicht kontinuierlich ändern und werden daher der Model-Loop auch nicht zur Behandlung übergeben.

In diesem Spiel gibt es nur eine Benutzeraktion, auf die minipong direkt reagieren muss: „Klick auf den Start-Stopp-Button“ . Die zweite Aktion „Bewegung des Schlägers“ wird vom Keyboard-Controller direkt an den Schläger weitergeleitet. Für die Aktion „Button-Klick“ muss minipong dem Button in jedem der beiden Spielzustände „Spiel gestoppt“ und „Spiel gestartet“ eine geeignete Callback-Funktion zuweisen, die die jeweils passende Aktion ausführt. Im Fall „Spiel gestoppt“ muss der Button-Klick das Spiel starten und im Fall „Spiel gestartet“ muss er das Spiel beenden.

Der Ball kann zwei weitere Aktionen ausführen. Er kann mit dem Schläger kollidieren oder das Spielfeld verlassen. Beide Aktionen werden von der Kollisionsbehandlung erkannt. Jedes mal, wenn eine wenn der Ball eine dieser beiden Aktionen durchführt muss die Prozedur „minipong“ darüber informiert werden, damit sie entsprechend reagieren kann. Hierzu definiert sie mit Hilfe der drei Hilfs-Prozeduren „collisionBallPaddle“, „collisionBallPaddle“ und „collisionStagePaddle“ eine geeignete Kollissionsprozedur, die sie der Model-Loop zur Kollisionserkennung und -behandlung übergibt.

Neues Projekt anlegen

Legen Sie ein neues Projekt mit dem Namen „MiniPong04“ an.

Erstellen Sie folgende Ordner:

  • web
  • web/css
  • web/json
  • web/js
  • web/js/lib
  • web/js/lib/require
  • web/js/app
  • web/js/app/collision
  • web/js/app/control
  • web/js/app/logic
  • web/js/app/model
  • web/js/app/view

Kopieren Sie folgende Dateien des Projekts MiniPong03 in das neue Projekt:

  • web/index.html (Ersetzen Sie im Titel „MiniPong03“ durch „MiniPong04“ und ersetzen Sie im Copyright-Kommentar meinen Namen durch Ihren Namen.)
  • web/css/main.css
  • web/js/lib/require/json.js
  • web/js/lib/require/require.js
  • web/js/lib/require/text.js

Fügen Sie in die Datei „index.html“ hinter dem Canvas-Element ein Button-Element ein:

<form>
  <button id="button_start_stop" type="button"></button>
</form>

Erstellen Sie die Datei „web/js/main.js“ und fügen Sie folgenden Code ein:

requirejs.config
({
  baseUrl: 'js', // By default load any modules from directory js
  paths :
  {
    app:       'app',

    model:     'app/model',
    view:      'app/view',
    control:   'app/control',
    logic:     'app/logic',
    collision: 'app/collision',

    loadjson:  'lib/require/json',
    text:      'lib/require/text',
    json:      '../json'
  }
});

requirejs
( ['loadjson!json/init.json', 'app/init'],
  function(initJSON, init)
  {
    //init(window, initJSON);
  }
);

Für jedes Modulpaket wurden ein Ordner angelegt und eine Kurzbezeichnung des Modulpfades festgelegt:

Farbe Ordner Modulpfad Modulzweck
grau js/app app Initialisierung der Anwendung
blau web/js/app/model model Model der Anwendung
grün web/js/app/view view Anwendungsview
orange web/js/app/controller controller Controller zur Behandlung der Benutzeraktionen
flieder web/js/app/logic logic Anwendungslogik
gelb web/js/app/collision collision Kollisionserkennung und -behandlung

Erstellen Sie für jedes Modul, das im Klassendiagramm aufgeführt ist (mit Ausnahme von ModelStage), eine leere Datei in der dieses Modul implementiert wird. Achten Sie darauf, dass die Farbmarkierung der Module mit den Farbmarkierungen der Modulordner übereinstimmt:

web/js/app

  • init.js

web/js/app/model

  • button.js
  • ball.js
  • paddle.js
  • text.js
  • loop.js

web/js/app/view

  • button.js
  • ball.js
  • paddle.js
  • text.js
  • loop.js

web/js/app/control

  • keyboard.js

web/js/app/logic

  • minipong.js

web/js/app/collision

  • ball_paddle.js
  • stage_ball.js
  • stage_paddle.js

Fügen Sie in jede Datei ein RequireJS-Kommando ein, das dafür sorgt, dass die benötigten Module geladen und die darin definierten Funktionen (d. h. Prozeduren bzw. Konstruktorfunktionen) dem Modul zur Verfügung gestellt werden. Welche Module ein Modul benötigt, erkennt man an den roten Pfeilen im Klassendiagramm.

Von den meisten Module geht kein roter Pfeil aus. In diese können Sie jeweils folgenden Code einfügen:

define
( [],
  function()
  { "use strict";

    return null;
  }
);

Von minipong gehen vier rote Pfeile aus. Das heißt, in die Datei „minipong.js“ müssen sie folgende RequireJS-Kommando einfügen:

define
(['collision/ball_paddle', 'collision/stage_ball', 'collision/stage_paddle',
    'model/loop'
  ],
  function(collisionBallPaddle, collisionStageBall, collisionStagePaddle,
           ModelLoop
  )
  { "use strict";

    return null;
  }
);

Die Reihenfolge, in der Sie die benötigten Module laden, ist unwichtig. Wichtig ist nur, dass die Reihenfolge der Kurzbezeichnungen der Modulpfade mit der Reihenfolge der Parameter in der Callback-Funktion übereinstimmt.

Das Init-Modul „app/init“ benötigt sehr viele andere Module, um seine Aufgabe zu erledigen. Entsprechend lang ist die Modulliste:

define
( ['model/button', 'view/button',
   'model/ball',   'view/ball',
   'model/paddle', 'view/paddle',
   'model/text',   'view/text',
   'view/loop',
   'control/keyboard',
   'logic/minipong'
  ],
  function(ModelButton, ViewButton,
           ModelBall,   ViewBall,
           ModelPaddle, ViewPaddle,
           ModelText,   ViewText,
           ViewLoop,
           controlKeyboard,
           minipong
          )
  { "use strict";

    return null;
  }
);

Jetzt fehlt noch die Datei „web/json/init.json“, die schon ziemliche Ausmaße angenommen hat, weil sie für (fast) jedes Modul im Diagramm geeignete Initialwerte enthält:

{
  "canvas":
  {
    "element": "canvas",
    "width":   400,
    "height":  300
  },

  "game":
  {
    "fps": 120,

    "welcome":   "Willkommen bei MiniPong",
    "ballLost":  "Das Spiel ist vorbei :-(",

    "startGame": "Spiel starten",
    "stopGame":  "Spiel beenden"
  },

  "model":
  {
    "buttonStartStop":
    {},

    "ball":
    {
      "r":   10,
      "pos": { "x": 195 , "y": 10 },
      "vel": { "x": { "min":  50, "max": 200 },
               "y": { "min": 150, "max": 200 }
             }
    },

    "paddle":
    {
      "width":    50,
      "height":    8,
      "pos":      {"x": 175, "y": 287},
      "vel":      {"x": 100, "y":   0},
      "acc":      {"x": 500, "y":   0},
      "friction": 0.3
    },

    "info":
    {
      "pos": {"x": 10, "y": 130}
    },

    "score":
    {
      "pos":      {"x": 5, "y": 3},
      "template": "Punkte: $1",
      "value":    0
    }
  },

  "view":
  {
    "buttonStartStop":
    {
      "elementID": "button_start_stop"
    },

    "ball":
    {
      "color":       "#55AA55",
      "borderWidth": 1,
      "borderColor": "#000000"
    },

    "paddle":
    {
      "color":       "#aa55cc",
      "borderWidth": 1,
      "borderColor": "#000000"
    },

    "info":
    {
      "color":     "#000000",
      "font":      "bold 25px Verdana, Geneva, sans-serif",
      "textAlign": "center"
    },

    "score":
    {
      "color":  "#000000",
      "font":   "bold 20px \"Courier New\", Courier, monospace",
      "textBaseline": "top"
    }
  },

  "control":
  {
    "player":
    {
      "left":  {"key": "ArrowLeft",  "keyCode": 37},
      "right": {"key": "ArrowRight", "keyCode": 39}
    }
  }
}

Wenn Sie alles richtig gemacht haben, sollten Sie jetzt die Datei „index.html“ im Browser fehlerfrei laden können. Diese Datei hat noch keine Inhalte, mit Ausnahme eines leeren Canvas-Elements. In der Browser-Konsole sollten aber keine Fehler gemeldet werden.

Es ist allerdings ziemlich unbefriedigend, wenn man eine Web-Anwendung ausführt und überhaupt nichts zu sehen ist, obwohl etwas passiert. Öffnen Sie in den Browser-Entwicklertools die Netzwerkansicht und laden Sie die HTML-Datei erneut. Dann sollten Sie sehen, dass 23 Dateien geladen werden. (Hier ist später noch Optimierungsarbeit angesagt. Die Anzahl der Dateien muss reduziert und die Inhalte müssen komprimiert werden. Das ist aber nicht Thema dieses Tutoriums.)

Sie können probehalber auch mal console.log-Befehle in die einzelnen Dateien einfügen, damit Sie sehen in welcher Reihenfolge die Module geladen werden.

console.log('Modul "MODULNAME" wird geladen'); 
// Ersetzen Sie MODULNAME durch den Namen des aktuellen Moduls.

define
( [],
  function()
  { "use strict";

    console.log('Modul "MODULNAME" wurde geladen'); 
    // Ersetzen Sie MODULNAME durch den Namen des aktuellen Moduls.

    return null;
  }
);

Als Musterlösung gibt es das Projekt WK_MiniPong04_empty (SVN). Die darin enthaltene Datei index.html verwendet allerdings den Befehl „terminal.log“, den Sie als Übungsaufgabe im Praktikum erstellen sollten. Damit werden die Log-Nachrichten im HTML-Dokument und nicht in der Browser-Konsole ausgegeben. Außerdem wurde der Logging-Code etwas erweitert, sodass anhand von Einrückungen zu sehen ist, welcher Code von welchem Modul geladen wird.

Models und Views

Ball

Ball-View und Ball-Model

In den vorangegangenen Teilen des Tutoriums war der Ball ständig sichtbar und ständig in Bewegung. Das heißt, es wurden nur folgende Attribute und Methoden benötigt:

  • r (Radius)
  • x (aktuelle $x$-Position)
  • y (aktuelle $y$-Position)
  • vx (aktuelle Geschwindigkeit in $x$-Richtung)
  • vy (aktuelle Geschwindigkeit in $y$-Richtung)
  • move(p_seconds) (Bewegen des Balls an die neue Position, die sich aus der aktuellen Position, der Geschwindigkeit und der Anzahl der Sekunden seit der letzten Berechnung der Position ergibt)

Im eigentlichen Spiel werden jedoch viel mehr Attribute und Methoden benötigt. Der Ball ist beim Start der Web-Anwendung zunächst unsichtbar, erst beim Spielstart wird er sichtbar gemacht

  • visible (Flag, ob der Ball sichtbar oder unsichtbar ist)
  • show() (Methode, um den Ball sichtbar zu machen)
  • hide() (Methode, um den Ball unsichtbar zu machen)

Solange der Ball unsichtbar ist, bewegt er sich nicht (vx === 0 und vy === 0) und befindet sich an seiner Startposition (x === x_start und y === y_start). Bei einem Reset des Spiels werden Position und Geschwindigkeit auf passende Startwerte gesetzt. Diese werden dem Konstruktor „ModelBall“ im Parameter „p_init“ übergeben und vom Konstruktor dann in folgenden Attribute gespeichert:

  • x_start ($x$-Koordinate der Startposition)
  • y_start ($y$-Koordinate der Startposition)
  • vx_start_min (minimale Startgeschwindigkeit in $x$-Richtung)
  • vx_start_max (maximale Startgeschwindigkeit in $x$-Richtung)
  • vy_start_min (minimale Startgeschwindigkeit in $y$-Richtung)
  • vy_start_max (maximale Startgeschwindigkeit in $y$-Richtung)

Man beachte, dass der Ball bei jedem Spielstart von derselben Position aus startet, aber dass die Startgeschwindigkeit (in Grenzen) zufällig gewählt wird.

Um das Spiel starten, beenden und neu starten zu können, werden drei weitere Methoden benötigt:

  • reset() (Ball anhalten, unsichtbar machen und auf die Startposition setzen)
  • start() (Ball auf eine zufällige Startgeschwindigkeit setzen)
  • stop() (Ball anhalten, d. h., die Geschwindigkeit auf Null setzen)

Insgesamt ergibt sich damit folgender Code:

Konstruktor

function ModelBall(p_init)
{
  this.r            = p_init.r;

  this.x_start      = p_init.pos.x;
  this.y_start      = p_init.pos.y;
  this.vx_start_min = p_init.vel.x.min;
  this.vx_start_max = p_init.vel.x.max;
  this.vy_start_min = p_init.vel.y.min;
  this.vy_start_max = p_init.vel.y.max;

  this.reset(); // initializes further attributes
}

Öffentliche Methoden

ModelBall.prototype =
{
  reset:
    function()
    {
      this.stop(); // By default, the ball does not move around.
      this.hide(); // By default, the ball is invisible.

      this.x = this.x_start;
      this.y = this.y_start;
    },

  show:
    function() { this.visible = true; },

  hide:
    function() { this.visible = false; },

  stop:
    function()
    {
      this.vx = 0;
      this.vy = 0;
    },

  start:
    function()
    {
      // react only if the ball is not already moving
      if (this.visible === true && this.vx === 0 && this.vy === 0)
      {
        this.vx = (Math.random() < 0.5 ? 1 : -1)*
                  (this.vx_start_min + 
                   Math.random()*(this.vx_start_max - this.vx_start_min)
                  );
        this.vy = (Math.random() < 0.5 ? 1 : -1)*
                  (this.vy_start_min +
                   Math.random()*(this.vy_start_max - this.vy_start_min)
                  );
      }
    },

  move:
    function(p_seconds)
    {
      this.x += this.vx * p_seconds;
      this.y += this.vy * p_seconds;
    }
};

Fügen Sie diesen Code in den Rumpf der Callback-Funktion in der Datei „model/ball.js“ ein. Vergessen Sie nicht, in dieser Callback-Funktionen den Return-Befehl

  return null;

durch den Return-Befehl

  return ModelBall;

zu ersetzen.

Die Ball-View ändert sich nur in einem Aspekt gegenüber der Version aus dem zweiten und dritten Teil des Tutoriums. Die Draw-Funktion darf den Ball nur zeichnen, wenn er sichtbar ist. Kopieren Sie also den Inhalt der Datei view/ball.js aus dem dritten Teil des Tutoriums in die neue Datei view/ball.js und fügen Sie die If-Anweisung

if (this.model.visible === true)

vor den eigentlichen Zeichenbefehl „p_context.drawImage“ ein.

Paddle

Schläger-Model und Schläger-View

Der Schläger ist sehr ähnlich aufgebaut wie der Ball. (Der Code ist also nicht DRY. Man sollte zwei allgemeine Klassen „ModelGeoObject“ und „ViewGeoObject“ definieren, von denen alle Geo-Objekt-Klassen wie „ModelBall“, „ViewBall“ gemeinsame Eigenschaften erben.)

Dem Schläger ist neben Position und Geschwindigkeit auch noch eine Beschleunigung zugeordnet. Für alle drei Vektoren sind feste Startwerte vorgegeben, d. h. der Schläger befindet sich bei Spielbeginn stets an derselben Stelle, startet, sobald der Benutzer ihn bewegt, mit derselben Geschwindigkeit und wird stets im gleichen Maße schneller, je länger der Spieler die entsprechende Steuertaste gedrückt hält.

Gegenüber dem Ball gibt es ein weiteres Attribut, das für die Realisierung der Use Cases wichtig ist:

  • friction (Reibung, genauer Reibungsfaktor)

Wenn sich der Schläger bei eine Kollision mit dem Ball bewegt, wird der Ball aufgrund der Reibung kurzzeitig vom Schläger in $x$-Richtung mitgezogen. Das heißt, die $x$-Geschwindigkeit des Schlägers ändert sich. Diese wird berechnet, indem zur $x$-Geschwindigkeit des Balls die $x$-Geschwindigkeit mal dem Reibungsfaktor des Schlägers addiert wird. Wenn die Reibung gleich Null ist hat also bei einer Kollision die Geschwindigkeit des Schlägers keine Auswirkung auf die Geschwindigkeit des Balls. Je größer der Reibungsfaktor gewählt wird, desto größer ist die Ablenkung. In der Musterlösung wurde der Reibungsfaktor auf $0,3$ gesetzt.

Zu guter Letzt werden noch vier berechnete Attribute für den Schläger definiert:

  • get left() { return this.x; } (linke $x$-Koordinate des Schlägers)
  • get right() { return this.x + this.width; } (rechte $x$-Koordinate des Schlägers)
  • get top() { return this.y; } (linke $y$-Koordinate des Schlägers)
  • get bottom() { return this.y + this.height; } (rechte $y$-Koordinate des Schlägers)

Diese Attribute vereinfachen die Kollisionsfunktionen etwas, bei denen mehrfach auf die verschiedenen Seiten des Schlägers zugegriffen werden muss.

Insgesamt sieht die Implementierung des Schlägermodells folgendermaßen aus:

Konstruktor

function ModelPaddle(p_init)
{
  this.width    = p_init.width;
  this.height   = p_init.height;

  this.x_start  = p_init.pos.x;
  this.y_start  = p_init.pos.y;
  this.vx_start = p_init.vel.x;
  this.vy_start = p_init.vel.y;
  this.ax_start = p_init.acc.x;
  this.ay_start = p_init.acc.y;

  this.friction = p_init.friction;

  this.reset(); // initializes further attributes
}

Öffentliche Methoden

ModelPaddle.prototype =
{
  reset:
    function()
    {
      this.stop(); // By default, the paddle does not move around.
      this.hide(); // By default, the paddle is invisible.

      this.x = this.x_start;
      this.y = this.y_start;
    },

  show:
    function()
    { this.visible = true; },

  hide:
    function()
    { this.visible = false; },

  stop:
    function()
    { this.vx = 0;
      this.vy = 0;

      this.ax = 0;
      this.ay = 0;
    },

  start:
    function(p_direction)
    {
      // react only if the paddle is visible not already moving
      if (this.visible === true &&
          this.vx === 0 && this.vy === 0
         )
      {
        switch (p_direction)
        {
          case "left":
            this.vx = -this.vx_start;
            this.ax = -this.ax_start;
            break;
          case "right":
            this.vx =  this.vx_start;
            this.ax =  this.ax_start;
            break;

          case "up":
            this.vy = -this.vy_start;
            this.ay = -this.ay_start;
            break;
          case "down":
            this.vy =  this.vy_start;
            this.ay =  this.ay_start;
            break;
        }
      }
    },

  move:
    function(p_seconds)
    { this.x  += this.vx * p_seconds;
      this.y  += this.vy * p_seconds;
      this.vx += this.ax * p_seconds;
      this.vy += this.ay * p_seconds;
    },

  /** The left side of the paddle (read only). */
  get left()   { return this.x; },

  /** The right side of the paddle (read only). */
  get right()  { return this.x + this.width; },

  /** The top side of the paddle (read only). */
  get top()    { return this.y; },

  /** The bottom side of the paddle (read only). */
  get bottom() { return this.y + this.height; }
};

Beachten Sie, dass es hier empfehlenswert ist, das Prototyp-Objekt des Konstruktors nicht mit mehrere Befehle der Art

ModelPaddle.prototype.reset = function(){...};
ModelPaddle.prototype.show = function(){...};
ModelPaddle.prototype.hide = function(){...};
...

zu befüllen, sondern ein eigenes Prototyp-Objekt zu definieren:

ModelPaddle.prototype =
{
  reset: function(){...},
  show:  function(){...},
  hide:  function(){...},
  ...
  get left() { return this.x; },
  get right()  { return this.x + this.width; },
  ...
};

Der Grund ist, dass es keine einfache Syntax gibt, eine Getter- oder einer Setter-Methode zu einem bestehenden Objekt hinzuzufügen. Im obigen Beispiel müsste man beispielsweise Folgendes schreiben, um Getter-Methoden zum Objekt „ModelPaddle.prototype“ nachträglich hinzuzufügen:

Object.defineProperty(ModelPaddle.prototype, 
                      'left', 
                      { get: function() { return this.x; } }
                     );
Object.defineProperty(ModelPaddle.prototype,
                      'right', 
                      { get: function() { return this.x + this.width; } }
                     );
...

Die Paddle-View ändert sich wiederum nur in einem Aspekt gegenüber der Version aus dem zweiten und dritten Teil des Tutoriums. Die Draw-Funktion darf den Schläger nur zeichnen, wenn er sichtbar ist. Kopieren Sie also den Inhalt der Datei view/paddle.js aus dem dritten Teil des Tutoriums in die neue Datei view/paddle.js und fügen Sie die If-Anweisung

if (this.model.visible === true)

vor den eigentlichen Zeichenbefehl „p_context.drawImage“ ein.

Text

Text-Model und Text-View

Im Spiel „MiniPong“ gibt es zwei Textfelder: Eines zum Anzeigen des Scores und eines zum Anzeigen von allgemeinen Informationen. Im Gegensatz zu Ball und Schläger bewegt sich ein Text (in diesem Spiel!) nicht. Daher benötigt man im Model wesentlich weniger Attribute und Methode.

  • x ($x$-Position des Text-Aufhängepunkt)
  • y ($y$-Position der Text-Baseline)
  • template (ein optionaler String, der die Zeichenfolge „$1“ enthält)
  • value (der Wert, der im Textfeld dargestellt werden soll)
  • text (ein Read-only-Attribut: „return template.replace('$1', value)“)
  • visible (Sichtbarkeit des Textes)

Das zugehörige View-Objekt legt das Aussehen des Textes fest:

  • color (Textfarbe)
  • font (Textfont, analog zu CSS-Fonts)
  • textAlign (Aufhängepunkt: left, center, right)
  • textBaseline (Baseline: top, bottom, middle, alphabetic, hanging)

Damit sollte eigentlich klar sein, wie die Model- und die View-Klasse aussehen- Fügen Sie diesen Code in den Rumpf der Callback-Funktion in der Datei „model/text.js“ ein. Vergessen Sie nicht, in dieser Callback-Funktionen den Return-Befehl

  return null;

durch den Return-Befehl

  return ModelText;

zu ersetzen.

ModelText: Konstruktor

function ModelText(p_init)
{
  this.x        = p_init.pos.x;
  this.y        = p_init.pos.y;

  this.template = p_init.template;
  this.value    = p_init.value;

  this.visible  = true;
}

ModelText: Öffentliche Methoden

ModelText.prototype =
{
  // read only attribute
  get text()
  { return (this.value == null)
           ? ''
           : (this.template == null)
             ? this.value.toString()
             : this.template.replace('$1', this.value);
  }
};

Die View-Klasse ist auch nicht viel komplexer. Auf die Vorberechnung eine Mini-Canvas, der anstelle des Textes in den Haupt-Canvas kopiert wird, wird hier verzichtet. Da sich der Text-Inhalt ändern kann und üblicherweise auch regelmäßig ändert, müsste man bei jeder Text-Änderung einen neuen derartigen Mini-Canvas erstellen. Das ist zwar möglich, führt hier aber zu weit.

ViewText: Konstruktor

function ViewText(p_model, p_init /*, p_document*/)
{
  this.model        = p_model;

  this.color        = p_init.color        || 'black';
  this.font         = p_init.font         || 'normal';
  this.textAlign    = p_init.textAlign    || 'left';
  this.textBaseline = p_init.textBaseline || 'alphabetic';
}

ViewText: Öffentliche Methoden

  ViewText.prototype.draw =
    function(p_context)
    {
      var l_model = this.model;

      if (l_model.value == null || l_model.value.toString() === '')
        return;

      // ALL font attributes must be set, as it cannot be
      // guaranteed that another module has changed some font
      // attributes before.
      p_context.font         = this.font;
      p_context.textAlign    = this.textAlign;
      p_context.textBaseline = this.textBaseline;
      p_context.fillStyle    = this.color;
      p_context.fillText(l_model.text,
                         (this.textAlign === 'center')
                           ? p_context.canvas.clientWidth/2
                           : l_model.x,
                         l_model.y
                        );
    };

Haben Sie an die Return-Anweisung „return ViewText;“ gedacht?

Button

Button-Model und Button-View

Einen grafischen Button, der im Canvas dargestellt wird, könnte man mit Hilfe eines Rechtecks oder Kreises und eines Textes realisieren. Button-Klicks müsste man dann mit Hilfe eine Kollisionserkennung („Mausspitze kollidiert mit Button“) und -behandlung verarbeiten.

Hier soll ein einfacherer Weg eingeschlafen werden: Als Button wird ein HTML-Button verwendet, der sich außerhalb der Bühne befindet. Folgende Attribute sind in einem Model-Objekt enthalten:

  • label (Der Text, mit dem der Button im HTML-Dokument beschriftet sein soll.)
  • class (Ein CSS-Klassen-Name, der dem Button-Element im HTML-Dokument zugeordnet wird. Beispielsweise kann man eine CSS-Klasse „.hidden“ definieren, mit dem man den Button unsichtbar machen kann.)
  • onClick (Eine Methode, die aufgerufen wird, sobald der Button geklickt wird.)

Der Konstruktor übernimmt für diese Attribute wie üblich alle Initialwerte aus einen Init-Objekt namens „p_init“. Ein Problem besteht dabei allerdings. Das Init-Objekt wird üblicherweise aus einer JSON-Datei eingelesen. Eine derartige Datei kann keine JavaScript-Funktionen enthalten. Das heißt, das Attribut „onClick“ wird üblicherweise mit „null“ initialisiert. Es ist dann die Aufgabe einer Logikkomponente diesem Attribut zur rechten Zeit eine geeignete Prozedur zuzuweisen.

ModelButton: Konstruktor

function ModelButton(p_init)
{
  this.label   = p_init.label || null;
  this.class   = p_init.class || null;
  this.onClick = p_init.onClick || null;
}

Die View ist auch nicht sonderlich kompliziert. Der Konstruktor speichert wie üblich das zugehörige Model im Attribut „model“. Außerdem sucht er im HTML-Dokument denjenigen Button, der mit dem Model verknüpft werden soll.

ViewButton: Konstruktor

function ViewButton(p_model, p_init, p_document)
{
  this.model   = p_model;
  this.element = p_document.getElementById(p_init.elementID);
}

Sie Draw-Methode macht nichts weiter, als alle Attribute, die im Model definiert sind, in das zugehörige HTML-Button-Element zu kopieren, sofern das jeweilige Attribut im Model definiert ist und sich vom im HTML-Button-Element gespeicherten Wert unterscheidet.

ViewButton: Öffentliche Methoden

ViewButton.prototype.draw =
  function()
  {
    var l_model   = this.model,
        l_element = this.element;

    if (l_model.label != null && l_element.innerHTML !== this.model.label)
    { l_element.innerHTML = this.model.label; }
    if (l_model.class != null && l_element.className !== this.model.class)
    { l_element.className = this.model.class; }
    if (l_model.onClick != null && l_element.onclick !== this.model.onClick)
    { l_element.onclick = this.model.onClick; }
  };

Da die Draw-Methode regelmäßig aufgerufen wird (ca. 60 mal pro Sekunde), ändert sich das Aussehen und/oder das Verhalten des Button automatisch mit jeder Änderung des Models.

Eigentlich ist das ein Overkill. So eine Änderung des Button-Labels und des Button-Verhaltens passiert nur ein paar mal pro Spiel. Hätte die Logik eine direkten Zugriff auf die View, könnte man es sich sparen, den Button durch die View-Loop aktualisieren zu lassen. Wenn man der Spiellogik – aus gutem Grund – keinen direkten Zugriff auf die View gewähren will und man den Button auch nicht viele Dutzend mal pro Sekunde aktualisieren will, hilft der Einsatz des so genannten Observer-Patterns weiter. Dies soll hier aber nicht weiter verfolgt werden.

Keyboard-Controller

Der Keyboard-Controller

Der Controller zum Steuern des Paddles per Tastatur ändert sich nicht. Er kann eins zu eins aus dem dritten Teil des Tutoriums übernommen und in die Datei controller/keyboard.js eingefügt werden.

Kollisionserkennung und -behandlung

Hilfsprozeduren für Kollisionserkennung und -behandlung

Im dritten Teil des Tutoriums wurde die Kollisionserkennung und -behandlung als Teil des Models angesehen. Ihre einzige Aufgabe war es, Geschwindigkeit und Position von beweglichen Objekten im Falle von Kollisionen zu korrigieren. Nun kommt noch eine weitere Aufgabe hinzu. Sie muss die Spiellogik über Kollisionen informieren, die das Spielgeschehen beeinflussen. Im Fall von MiniPong sind das die Kollision von Schläger und Ball (Punktgewinn) sowie die Kollision von Schläger und unterem Bühnenrand (Spielende).

Bislang gab es lediglich ein Modul namens „[1]“ zur Kollisionsbehandlung. Dieses Modul droht zum Giganten zu werden, wenn immer mehr und mehr Kollisionsarten behandelt werden müssen. Daher wird es in mehrere Teilmodule aufgespalten.

Die Kollisionsprozedur „collisionStagePaddle“ kann eins zu eins vom dritten Teil des Tutoriums übernommen werden, da die Kollision des Paddles mit der Wand keine Auswirkung auf das Spielgeschehen hat. Diese Kollision bewirkt lediglich, dass der Schläger gestoppt wird. Und das erledigt die Prozedur von sich aus. Allerdings gibt es durchaus Situationen, in denen auch eine Kollision zwischen Schläger und Wand von der Spiellogik behandelt werden muss. Beispielsweise kann man im Breakout-Spiel Bolo mit dem Schläger gegen die Wand „donnern“. Wenn man dies fest genug macht, wenn also der Schläger bei der Kollision mit der Wand eine gewisse Geschwindigkeit hat, wird der Raum leicht erschüttert. Dies hat auch Auswirkungen auf den Ball, dessen Geschwindigkeitsvektor dadurch leicht verändert wird. Auf diese Weise kann man den Ball, wenn er irgendwo feststeckt, häufig wieder frei bekommen.

Die folgende Implementierung der Kollisionsprozedur „collisionStagePaddle“ unterscheidet sich in einer Hinsicht von der ursprünglichen Implementierung. Anstatt die Ränder des Schlägers zu berechnen, werden die neuen Schläger-Attribute „left“, „right“, „top“ und „bottom“ verwendet. Fügen Sie diesen Code ins Modul „collision/stage_paddle“ ein (und vergessen Sie nicht, den Return-Befehl anzupassen).

function collisionStagePaddle(p_stage, p_paddle)
{
  // If the paddle collides with the left wall of the stage,
  // stop it and move it back to the stage.
  if (p_paddle.vx < 0 && p_paddle.left <= 0)
  {
    p_paddle.stop();
    p_paddle.x = 0;
  }

  // If the paddle collides with the right wall of the stage,
  // stop it and move it back to the stage.
  if (p_paddle.vx > 0 && p_paddle.right >= p_stage.width)
  {
    p_paddle.stop();
    p_paddle.x = p_stage.width - p_paddle.width;
  }

  // If the paddle collides with the top wall of the stage,
  // stop it and move it back to the stage.
  if (p_paddle.vy < 0 && p_paddle.top <= 0)
  {
    p_paddle.stop();
    p_paddle.y = 0;
  }

  // If the paddle collides with the bottom wall of the stage,
  // stop it and move it back onto the stage.
  if (p_paddle.vy > 0 && p_paddle.bottom >= p_stage.height)
  {
    p_paddle.stop();
    p_paddle.y = p_stage.height - p_paddle.height;
  }
}

Die Kollisionsprozedur „collisionStageBall“ ist im Modul „collision/stage_ball“ enthalten. Die Prozedur kann allerdings nicht eins zu eins vom dritten Teil des Tutoriums übernommen werden, da die Kollision mit der unteren Wand anders behandelt werden muss, als die Kollision mit den übrigen Wänden.

Bei einer Kollision mit der unteren Wand wird das Spiel beendet. Für die Behandlung des Spielendes ist die Spiellogik zuständig. Um über diese Art der Kollision benachrichtigt zu werden, übergibt sie der Kollisionsprozedur „collisionStageBall“ im Parameter „cb_hit“ eine Callback-Funktion, die im Falle einer Kollision mit der unteren Wand aufgerufen werden soll. Damit die Spiel erst endet, wenn der Ball die Bühne vollständig verlassen hat, wird die untere Wand etwas nach unten in den nicht sichtbaren Bereich verschoben.

function collisionStageBall(p_stage, p_ball, cb_exit)
{
  // If the ball collides with the left or the right wall of the stage
  // mirror its x-velocity and move the ball back onto the stage.
  if (p_ball.x <= p_ball.r)
  {
    p_ball.vx = -p_ball.vx;
    p_ball.x += 2*(p_ball.r - p_ball.x);
  }
  if (p_ball.x >= p_stage.width - p_ball.r)
  {
    p_ball.vx = -p_ball.vx;
    p_ball.x -=  2*(p_ball.r - p_stage.width + p_ball.x);
  }

  // If the ball collides with the top wall of the stage
  // mirror its y-velocity and move the ball back onto the stage.
  if (p_ball.y <= p_ball.r)
  {
    p_ball.vy = -p_ball.vy;
    p_ball.y += 2*(p_ball.r - p_ball.y);
  }

  // If the ball leaves the bottom of the stage, call cb_exit.
  // Factor 1.5: The ball is really outside the stage an thus invisible,
  // even if the view draws a very thick border around it.
  if (p_ball.y >= p_stage.height + 1.5*p_ball.r)
  { if (cb_exit) 
      cb_exit(); 
  }
}

Zu guter Letzt muss noch die Kollisionserkennung und -behandlung für Kollisionen des Balls mit dem Schläger realisiert werden. Über jede derartige Kollision wird die Spiellogik ebenfalls mit einer Callback-Funktion informiert, damit letztere den Punktestand aktualisieren kann.

Die nachfolgende Implementierung der Kollisionserkennung und -behandlung ist ziemlich primitiv, da nur zwei Fälle unterschieden werden: Kollision des Balles mit der oberen oder der unteren Seite des Schlägers. Das reicht zunächst für unsere Zwecke, führt aber schon zu Problemen, wenn ein senkrechter Schläger an einer Seitenwand verwendet werden soll. In diesem Fall müsste die Kollisionsprozedur umgeschrieben werden.

Bei einer korrekten Kollisionserkennung und -behandlung zwischen Kreis und Rechteck müssen 8 Fälle unterschieden werden: Kollision des Balls mit einer der vier Seitenwände sowie Kollision des Balls mit einer der vier Ecken.

function collisionBallPaddle(p_ball, p_paddle, cb_hit)
{
  if (p_ball.y + p_ball.r     >= p_paddle.top    &&
      p_ball.y + p_ball.r     <= p_paddle.bottom &&
      p_ball.x + 0.5*p_ball.r >= p_paddle.left   &&
      p_ball.x - 0.5*p_ball.r <= p_paddle.right
     )
  {
    // Resolve penetration.
    if (p_ball.vy > 0)        // The ball is moving from top to bottom.
    { p_ball.y = p_paddle.top - p_ball.r; }
    else                       // The ball is moving from bottom to top.
    { p_ball.y = p_paddle.bottom + p_ball.r; }

    // Modify the velocity of the ball.
    p_ball.vy = -p_ball.vy;
    p_ball.vx += p_paddle.friction*p_paddle.vx;

    // If the paddle hits the ball, invoke the callback function.
    if (cb_hit) 
    {  cb_hit(); }
  }
}

Model- und View-Loop

Model-Loop und View-Loop

Die Model-Loop sowie die View-Loop waren bislang Bestandteile des Moduls „minipong.js“. Da dieses Modul deutlich größer wird, sollte es zerschlagen werden. Für die MiniPong-Anwedung wird es in vier Einzelmodule unterteilt:

  • init (Erzeugung aller wesentlichen Objekte; Starten der View Loop; Verknüpfung des Keyboard-Controllers mit dem Paddle)
  • ViewLoop (Visualisierung der aktuellen Zustände der grafischen Objekte des Spiels)
  • ModelLoop (Berechnung der Positionen der beweglichen Objekte des Spiels; Information der Spiellogik über bestimmte Kollisionen)
  • minipong (Die Spiellogik)

Zunächst werden die beiden Loops in eigenständige Module ausgelagert. Beide Module werden als Klassen realisiert. Das heißt, es müssen jeweils ein ViewLoop- und ein ModelLoop-Objekt erzeugt werden. Diese Objekte kennen jeweils zwei Methoden start und stop, den denen die Loops gestartet und auch wieder angehalten werden können. Im Falle der View-Loop ist das für das Spiel MiniPong nicht sonderlich wichtig, da diese View nur einmal gestartet wird und dann dauerhaft aktiv ist. Bei einer komplexeren Web-Anwendung solle man allerdings versuchen, die View-Loop nur während des eigentlichen Spiels zu aktivieren. So eine Loop kostet Rechenpower und belastet damit insbesondere den Akku von mobilen Geräten.

Die Model-Loop muss auch schon von der MiniPong-Spiellogik gestartet und gestoppt werden können. Dies wird insbesondere bei einer erweiterten Variante deutlich, bei dem das Spiel durch eine Pause-Taste temporär angehalten werden kann (index_pause.html).

Die View-Loop erhält als Argumente das Fenster in dem die Web-Anwendung läuft, einen Canvas, auf dem grafische Objekte visualisiert werden sollen sowie eine Liste, die die Views dieser Objekte enthält. Die View-Loop löscht zu Beginn den Canvas und ruft dann für alle View-Objekte die Methode „draw“ auf. Dieser übergibt sie als Argument den 2D-Context des Canvas-Elements. Allerdings ist die Draw-Methode nicht verpflichtet, ein Objekt auf den Canvas zu zeichnen. Sie kann auch ein Objekt im DOM-Baum des HTML-Dokuments aktualisieren. Dies macht beispielsweise die Draw-Methode des Button-Objekts.

ViewLoop: Konstruktor

function ViewLoop(p_window, p_canvas, p_views)
{
  var l_context = p_canvas.getContext("2d"),
      n         = p_views.length;

  this.v_window = p_window;

  this.m_update_view =
    function m_update_view()
    {
      // clear canvas
      l_context.clearRect(0, 0, p_canvas.width, p_canvas.height);

       // draw all visual objects
      for (var i = 0;  i <n; i++ )
      { p_views[i].draw(l_context); }

      p_window.requestAnimationFrame(m_update_view);
    };
}

Aktiviert und am Laufen gehalten wird die View-Loop wie üblich mit Hilfe der Methode „window.requestAnimationFrame“. Diese Methode liefert beim Aufruf eine Integerzahl zurück, die den Timer eindeutig identifiziert. Wenn man diesen Identifikator speichert, kann man die rekursiven Aufrufe der Methode „window.cancelAnimationFrame“ unterbrechen und so die Loop anhalten. Dies wird ausgenutzt, um die Start- und die Stopp-Methode zu realisieren.

ViewLoop:Öffentliche Methoden

ViewLoop.prototype =
{
  start:
    function()
    {
      if (this.v_timer == null)
      { this.v_timer = this.v_window.requestAnimationFrame(this.m_update_view); }
    },

  stop:
    function()
    {
      if (this.v_timer != null)
      {
        this.v_window.cancelAnimationFrame(this.v_timer);
        delete this.v_timer;
      }
    }
};

Der Model-Loop-Konstruktor erwartet als Input eine Kollisionsprozedur, die (angestrebte) Update-Frequenz sowie eine Liste von Model-Objekten, die bewegt werden sollen.

Der Konstruktor definiert ein „privates“ Attribut „v_milliseconds“ und eine „private“ Methode „m_update_model“ auf die die Start- und die Stop-Methode zugreifen können. (In Wirklichkeit handelt es sich um ein öffentliches Attribut und um eine öffentliche Methode, da im Prototype-Objekt keine privaten Elemente definiert werden können. Ich kennzeichne private Elemente einfach mittels eines Namenszusatzes „v_...“ – v für „(Zustands-)Variable“ – bzw. „m_...“ – m für „Methode“ – und weiß damit, dass ich von außerhalb nicht auf derartige Elemente zugreifen darf.)

Die Methode „m_update_model“ ruft zunächst für alle Objekte die Move-Methode auf und führt anschließend (a posteriori) mit Hilfe der Kollisionsprozedur eine Kollisionserkennung und -behandlung durch.

ModelLoop: Konstruktor

function ModelLoop(p_collision, p_f, p_models)
{
  var l_seconds = 1 / p_f;
  this.v_milliseconds = 1000 * l_seconds;

  this.m_update_model =
    function ()
    {
      // move around all movable objects
      for (var i = 0, n = p_models.length; i < n; i++)
      { p_models[i].move(l_seconds); }

      // detect and handle collision (a posteriori)
      p_collision();
    };
}

Die Model-Update-Methode soll regelmäßig alle v_milliseconds Millisekunden aufgerufen werden. Dazu wird die JavaScript-Funktion „setInterval“ verwendet (die allerdings die Zeitvorgaben nicht sonderlich genau nimmt). Diese Funktion liefert, wie auch schon requestAnimationFrame, beim Aufruf eine Integerzahl zurück, mit der der Timer eindeutig identifiziert wird. Mit Hilfe dieses Identifikators und der Funktion „clearInterval“ kann man den Timer anhalten. Die Star- und die Stopp-Methoden können daher auf genau dieselbe Art realisiert werden, wie bei der View-Loop:

ModelLoop:Öffentliche Methoden

ModelLoop.prototype =
{
  start:
    function()
    {
      if (this.v_timer == null)
      { this.v_timer = setInterval(this.m_update_model, this.v_milliseconds) }
    },

  stop:
    function()
    {
       if (this.v_timer != null)
       {
         clearInterval(this.v_timer);
         delete this.v_timer;
       }
    }
};

Initialisierung

Initialisierung der Web-Anwendung

Die Initialisierungsprozedur „init“ hat zwei Aufgaben: Zunächst muss sie alle wesentlichen Objekte erstellen und anschließend die Anwendung zu Laufen bringen.

Sie erwartet zwei Argumente: das Fenster, in dem die Anwendung läuft und das Initialisierungsobjekt, das in der JSON-Datei definiert wurde:

function init(p_window, p_init)

Anschließend muss sie die benötigten Objekte erstellen bzw. aus dem HTML-Dokument extrahieren.

Die View-Objekte benötigen das im Browser-Fenster enthalten HTML-Dokument sowie das darin enthalten Canvas-Element. Auch der Keyboard-Controller benötigt das HTML-Dokument.

var l_canvas_init = p_init.canvas,
    l_document    = p_window.document,
    l_canvas      = l_document.getElementById(l_canvas_init.element),

Als nächstes müssen alle Model- und View-Objekte erzeugt werden, mit Ausnahme des ModelStage-Objekts. (Als ModelStage-Objekt wird das Canvas-Init-Objekt verwendet. Es enthält die Größe und die Breite der Bühne, und das ist alles was die Kollisionsprozedur von der Bühne wissen muss.)

Jedem Model-Objekt werden die Initialisierungsinformationen übergeben, die für das jeweilige Objekt im Initialisierungsobjekt „p_init“ enthalten sind. Jedem View-Objekt werden nicht nur Initialisierungsinformationen aus dem p_init-Objekt übergeben, sondern auch das Model, das es darstellen soll. Darüber hinaus benötigen einige Objekte das Objekt „p_dokument“, um einen Mini-Canvas zum Cachen der grafischen Darstellung des Models erstellen zu können.

l_model_button = new ModelButton(p_init.model.buttonStartStop),
l_view_button  = new ViewButton(l_model_button, p_init.view.buttonStartStop, l_document),

l_model_ball   = new ModelBall(p_init.model.ball),
l_view_ball    = new ViewBall(l_model_ball, p_init.view.ball, l_document),

l_model_paddle = new ModelPaddle(p_init.model.paddle),
l_view_paddle  = new ViewPaddle(l_model_paddle, p_init.view.paddle, l_document),

l_model_info   = new ModelText(p_init.model.info),
l_view_info    = new ViewText(l_model_info, p_init.view.info),

l_model_score  = new ModelText(p_init.model.score),
l_view_score   = new ViewText(l_model_score, p_init.view.score),

Als nächstes werden die soeben erzeugten Model- und View-Objekte in Container gesteckt, damit sie möglichst einfach an die Spiellogik bzw. die View-Loop übergeben werden können.

Die Modelle werden in ein Hasharray (= JavaScript-Objekt) gepackt, da die Spiellogik namentlich auf die Objekte zugreifen können muss. für die View-Loop wird ein einfaches Array als Container eingesetzt, da diese einfach der Reihe nach für alle View-Objekte die Draw-Methode aufruft.

l_models = { stage:  l_canvas_init,
             button: l_model_button,
             ball:   l_model_ball,
             paddle: l_model_paddle,
             info:   l_model_info,
             score:  l_model_score
           },

l_views  = [ l_view_button, l_view_ball, l_view_paddle, l_view_info, l_view_score];

Wie üblich muss noch die Größe des Canvas angepasst werden. Diese Größe ist wie immer im Objekt „l_canvas_init“ enthalten.

l_canvas.width  = l_canvas_init.width;
l_canvas.height = l_canvas_init.height;

Zu guter Letzt muss Init-Prozedur eine View-Loop erzeugen und starten (das geht in einem Aufwasch, die die Loop nicht mehr angehalten werden soll), den Keyboard-Controller dem Paddle zuweisen und die Spiellogik starten.

Bei jedem dieser drei Aufrufe übergibt sie einige der zuvor erzeugten Objekte:

  • ViewLoop: alle View-Objekte
  • controlKeyboard: das Paddle-Model
  • minipong: alle Model-Objekte
new ViewLoop(p_window, l_canvas, l_views).start();
controlKeyboard(p_window, p_init.control.player, l_model_paddle);
minipong(p_init.game, l_models);

MiniPong

MiniPong: Die Spiellogik

Nachdem das Modul „logic/minipong“ von allen übrigen Aufgaben befreit wurde, ist es nur noch für die Umsetzung der Spiellogik verantwortlich.

Sie erhält von der Init-Prozedur die für sie bestimmten Initialisierungswerte sowie die Model-Objekte, die sie manipulieren kann und soll.

function minipong(p_init, p_models)

Zunächst speichert sie alle Model-Objekte in lokalen Variablen. Das hat den Vorteil, dass sie nicht immer Aufrufe der Art „p_models.xyz“ tätigen muss. Außerdem legt sie ein leeres Array „l_models_movable“ an, das später alle beweglichen Model-Objekte enthält, d. h. alle Model-Objekte, für die die Methode „l_move“ existiert. Zu guter Letzt legt sie in der Variablen „l_model_loop“ eine Model-Loop an, die die Position und Geschwindigkeit der beweglichen Objekte regelmäßig aktualisiert. Als Kollisionsprozedur wird diesem Objekt die Funktion „f_collision“ übergeben. Diese Funktion wird weiter unten im Funktionsrumpf von minipong definiert.

var l_stage          = p_models.stage,
    l_button         = p_models.button,
    l_info           = p_models.info,
    l_score          = p_models.score,
    l_ball           = p_models.ball,
    l_paddle         = p_models.paddle,
    l_models_movable = [],
    l_model_loop     = new ModelLoop(f_collision, p_init.fps, l_models_movable);

Nun ist es an der Zeit, das Array „l_models_movable“ zu befüllen. Dazu wird einfach die Liste mit allen Model-Objekt durchlaufen. Alle Objekte, die die Methode „move“ enthalten werden in dieses Array eingefügt.

// Store all model objects that have a move method within the array l_models_movable.
for (var k in p_models)
{ if (p_models.hasOwnProperty(k) && p_models[k].move != null)
  { l_models_movable.push(p_models[k]); }
}

Mit den letzten beiden Anweisunges startet „minipong“ die Anwendung. Sie ruft dazu die Prozedur „f_stop“ auf (da der Benutzer das Spiel erst mittel eine Klicks auf den Start-Knopf starten muss) und schreibt eine Willkommensbotschaft in Info-Textfeld (und überschreibt damit die Meldung, die die Stopp-Funktion ins Info-Textfeld geschrieben hat.).

// Stop the game and display a welcome message.
f_stop();
l_info.value = p_init.welcome;

Nun müssen noch vier Prozeduren definiert werden, die in verschiedenen Spielsituationen aufgerufen werden. Alle vier Prozeduren werden im Rumpf der Prozedur „minipong“ definiert. Das heißt, nur minipong kann auf diese Prozeduren zugreifen. Sie kann sie allerdings als Callback-Funktionen an andere Prozeduren und Methoden weiterleiten. Und genau das macht sie auch.

Ganz wichtig für die Spiellogik ist die Definition einer geeigneten Kollisionsprozedur, die der Model-Loop übergeben wird, um die Kollisionserkennung und -behandlung damit durchzuführen. Diese Prozedur verwendet die drei Hilfsprozeduren „collisionBallPaddle“, „collisionStageBall“ und „ collisionStagePaddle“, die einfach nacheinander aufgerufen werden.

Zwei dieser Prozeduren erwarten als Input eine Callback-Funktion, die im Falle von bestimmten Kollisiones von der Kollisionsbehandlung aufgerufen werden. Die Prozedur „collisionBallPaddle“ ruft die Callback-Funktion auf, sobald der Ball mit dem Schläger kollidiert. In diesem Fall soll der Punktestand erhöht werden. Das wird mit einer sehr einfachen anonymen Prozedur erledigt:

function(){ l_score.value++; }

Diese Prozedur erhöht bei jedem Aufruf im Text-Feld „score“ den Wert „value“ um eins. Sobald ein neues Spiel gestartet wird, wird dieser Vert auf 0 gesetzt.

Die Prozedur „collisionStageBall“ informiert minipong mittels Callback, wenn der Ball die Bühne verlässt. Als Callback-Funktion wird dieser Prozedur die Prozedur „f_stop“ (siehe unten) übergeben, um das Spiel zu beenden.

// Collision detection and handling.
function f_collision()
{
  collisionBallPaddle(l_ball, l_paddle, function(){ l_score.value++; });
  collisionStageBall(l_stage, l_ball, f_stop);
  collisionStagePaddle(l_stage, l_paddle);
}

Ganz zu Beginn des Spiels, bei einem Abbruch durch den Spieler mittel Button-Klick und sobald der Ball die Bühne verlässt wird das Spiel beendet. Dies ist die Aufgabe der Prozedur „f_stop“.

Sie hält die Model-Loop und Ball an und macht Ball und Schläger unsichtbar. Dann ändert sie das Aussehen und das Verhalten des Start-Stopp-Buttons: Sie weißt ihm das Label „p_init.startGame“ (=== "Spiel starten" gemäß init.json) und die Prozedur „p_start“ zu. Das heißt, bei einem Klick auf diesen Button wird die Prozedur „p_start“ ausgeführt. Zu guter Letzt schreibt sie ins Info-Textfeld eine Nachricht, dass das Spiel beendet ist. Diese Nachricht kann man überschreiben, indem man direkt im Anschluss an einen Aufruf von „p_stop“ eine andere Nachricht ins Info-Textfeld schreibt. Dies geschieht direkt nachdem die Web-Anwendung gestartet wurde (siehe oben),

// Stop the game.
function f_stop()
{
  l_model_loop.stop();
  l_ball.stop();
  l_ball.hide();
  l_paddle.hide();

  l_button.label   = p_init.startGame;
  l_button.onClick = f_start;

  l_info.value     = p_init.ballLost;
}

Wenn das Spiel angehalten wurde, kann man es mit einem Klick auf den Start-Stopp-Button starten. Die zugehörige Start-Prozedur muss das Spiel zunächst zurücksetzen. Sie setzt den Score-Wert auf 0, löscht den Inhalt des Info-Textfeldes und setzt Schläger und Ball zurück an die Startpositionen. Dann ändert sie das Aussehen und das Verhalten des Start-Stopp-Buttons: Sie weißt ihm das Label „p_init.stopGame“ (=== "Spiel beenden" gemäß init.json) und die Prozedur „p_stop“ zu. Das heißt, bei einem Klick auf diesen Button wird die Prozedur „p_stop“ ausgeführt und das Spiel sofort beendet.

Nun kann sie das eigentliche Spiel starten. Dazu macht sie Schläger und Ball sichtbar und startet dann den Ball und die Model-Loop. Jetzt sind die Call-Backfunktionen der Kollisionsprozedur scharfgeschaltet. Das heißt, Kollisionen vom Schläger mit dem Ball werden mit einem Puktgewinn belohnt (dieser wird durch die View-Loop auch sofort angezeigt). Wenn der Ball die Bühne verlässt oder wenn der Benutzer den Start-Stopp-Button drückt, wird das Spiel beendet. Der erspielte Score ist zu diesem Zeitpunkt noch sichtbar. Erst mit einem neuen Spielstart wird er wieder auf 0 gesetzt.

// Start the game.
function f_start()
{
  l_score.value = 0;
  l_info.value  = '';
  l_ball.reset();
  l_paddle.reset();

  l_button.label   = p_init.stopGame;
  l_button.onClick = f_stop;

  l_paddle.show();
  l_ball.show();
  l_ball.start();
  l_model_loop.start();
}

Main

Jetzt müssen Sie in der Datei „main“ noch den Kommentar vor dem Aufruf der Prozedur „init“ löschen.

Damit wurden alle Module vollständig erstellt und MiniPong sollte gespielt werden können.

Quellen

  1. Braun (2011): Herbert Braun; Webanimationen mit Canvas; in: c't Webdesign; Band: 2011; Seite(n): 44–48; Verlag: Heise Zeitschriften Verlag; Adresse: Hannover; 2011; Quellengüte: 5 (Artikel)
  2. Kowarschick (MMProg): Wolfgang Kowarschick; Vorlesung „Multimedia-Programmierung“; Hochschule: Hochschule Augsburg; Adresse: Augsburg; Web-Link; 2018; Quellengüte: 3 (Vorlesung)