Spieleentwicklung mit JavaScript - Spielfeld
Aus XT Knowledge Base
Inhaltsverzeichnis |
Spielfeld zeichen
Zum Einstieg in die Entwicklung wird ein Schachbrettmuster erzeugt. Zu diesem Zweck wird eine neue Funktion implementiert, die anstelle des Codes aufgerufen wird, der das Spielfeld himmelblau eingefärbt hat.
Die Funktion
Im folgenden wird nur noch der Script-Teil der HTML-Datei dargestellt, solange sich der Rest der HTML-Datei nicht ändert.
<script type="text/javascript"> var canvas; var context; var fields = 8; var black_color = '#000000'; var white_color = '#ffffff'; function init() { canvas = document.getElementById("board"); context = canvas.getContext("2d"); paint_chess_board(); } function paint_chess_board() { var w = canvas.width / fields; var h = canvas.height / fields; for( var y = 0; y < fields; y++ ) { for( var x = 0; x < fields; x++ ) { context.fillStyle = ((x+y)&1) ? black_color : white_color; context.fillRect( w * x, h * y, w, h ); } } } </script>
Beschreibung
Die Funktion paint_chess_board() ist relativ kompakt implementiert und beinhaltet einige fortgeschrittene Sprachkonstrukte, mit der Anfänger sicher Schwierigkeiten haben werden. Deshalb werde ich diese Funktion nun im Detail beschreiben:
Ein Schachbrett besteht aus 8x8 Feldern.
Wenn man den Code anguckt, sieht man auf den ersten Blick keine 8, sondern eine globale Variable namens fields, die mit dem Wert 8 initialisiert wurde. Warum so umständlich?
Ein erfahrener Programmierer wird vermeiden, Literale in seinem Sourcecode zu verwenden. Literale sind Zahlen und Zeichenketten, die einen bestimmten Wert oder Text darstellen. Solche Werte werden in Konstanten oder Variablen gespeichert und im weiteren Programmablauf verwendet, was es vereinfacht, diese Werte nachträglich zu ändern. In unserem Fall hätten wir anstelle der Variablen 'fields' vier Mal den Wert 8 in unserer Funktion stehen. Würden wir uns plötzlich für ein Spielfeld mit 10x10 Feldern entscheiden, hätten wir das Problem alle diese Zahlen abzuändern. Das mag in einem kleinen überschaubaren Programm kein Problem sein, aber so ein Programm wächst schnell und solche Änderung können ganz schnell in einer Fehlersuch-Orgie ausarten, die nur unnötig Zeit verschlingt.
In unserem Fall verwenden wir immer die Variable fields, wenn wir die Anzahl der Felder in horizontaler oder vertikaler Richtung angeben möchten. Die Anzahl der Felder kann nun mit einem minimalen Aufwand angepasst werden.
Es ist wichtig, sich diese Methode von Anfang anzugewöhnen, auch wenn es auf den ersten Blick übertrieben aussieht. Aber die Flexibilität und Wartbarkeit des Programms steigt dadurch enorm und dadurch auch die Qualität des Programms.
Im ersten Schritt wird die Breite und Höhe eines einzelnen Feldes ermittelt. Zu beachten ist, dass wir von einem quadratischen Spielfeld ausgehen. Selbstverständlich wäre es denkbar, die Anzahl der Felder in x- und y-Richtung separat anzugeben.
Die verschachtelte Schleife dient dem Zweck, der Reihe nach alle Felder des Spielbretts aufzusuchen und dessen Farbe zu zeichnen. Insgesamt wird der innere Teil der Schleife also fields * fields Mal durchlaufen, was in unserem Fall 64 ergibt.
Jedes Feld wird mit einem Rechteck versehen, dessen Farbe hier auf relativ komplizierter Art und Weise ermittelt wird:
context.fillStyle = ((x+y)&1) ? black_color : white_color;
In dieser Anweisung stecken drei Spezialitäten:
- aus der x und y Position des aktuell zu zeichnenden Feldes wird ermittelt, welche Farbe es haben soll.
- eine binäre Und-Verknüpfung zweier Zahlen
- ein sogenannter trinärer Operator
Der Algorithmus
In der verschachtelten Schleife wird die Variable y von 0 bis 7 hochgezählt und somit insgesamt 8 Mal durchlaufen. Bei jedem Schleifendurchlauf wird dann die zweite Schleife ausgeführt, wobei dann jeweils x von 0 bis 7 hochgezählt wird. Der Inhalt der inneren Schleife wird also 64 Mal durchlaufen, wobei alle Kombinationen von x und y auftreten. Folgende Tabelle zeigt die Ergebnisse, wenn in jedem Feld x und y addiert werden:
x+y | x:0 | x:1 | x:2 | x:3 | x:4 | x:5 | x:6 | x:7 |
y:0 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
y:1 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
y:2 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
y:3 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
y:4 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
y:5 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
y:6 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
y:7 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
Man kann leicht sehen, dass die geraden und ungeraden Zahlen perfekt in der Weise abwechseln, wie es für unsere Zwecke nützlich ist. Ein Schachspieler weiß, dass A1 (das untere linke Feld) immer schwarz ist. Folglich werden alle Felder mit geraden Zahlen weiß und die mit den ungeraden Zahlen schwarz gefärbt.
Wie kann man nun ermitteln, ob eine Zahl gerade oder ungerade ist?
Per Definition sind alle Zahlen gerade, die ohne Rest durch zwei teilbar sind. Es gibt drei Möglichkeiten, das herauszufinden, wobei die Zahl immer dann gerade ist, wenn der Ausdruck true ergibt:
- a/2 == Math.floor(a/2); // Math.floor() schneidet den Nachkomma-Anteil einer Zahl ab
- a%2 == 0; // hier wird der Modulo (%) also der Rest einer Division berechnet.
- a&1 == 0; // die Zahl ist dann gerade, wenn das niedrigste Bit nicht gesetzt ist.
Variante 1 ist diejenige, die von Anfängern am häufigsten benutzt wird, allerdings am meisten Rechenzeit benötigt.
Variante 2 wird von Mathematikern bevorzugt.
Variante 3 ist diejenige, die am wenigsten Rechenzeit erfordert und von Programmierern gewählt wird, die mit der Natur binärer Zahlen vertraut sind.
Nach diesem kleine Exkurs sollte der Ausdruck (x+y)&1 sofort verständlich sein: Er ergibt 1 bei ungeraden Zahlen und 0 bei geraden Zahlen.
context.fillStyle = ((x+y)&1) ? black_color : white_color;
Was ist ein trinärer Operator?
Den oben genannten Ausdruck könnte man wesentlich lesbarer auch so formulieren:
if( (x+y)&1 == 1 ) { context.fillStyle = black_color; } else { context.fillStyle = white_color; }
Solche Programm-Konstrukte kommen sehr häufig vor. Da Programmierer dem Vorurteil nach sehr faule (oder wie sie selbst sagen würden, effiziente) Leute sind, hat sich einer von ihnen eine verkürzte Schreibweise ausgedacht:
if( (Bedingung) ) { (Ausdruck, wenn erfüllt) } else { (Ausdruck, wenn nicht erfüllt) }
wird dann zu:
(Bedingung) ? (Ausdruck, wenn erfüllt) : (Ausdruck, wenn nicht erfüllt)
Zusätzlich kann die verkürzte Konstruktion selbst als Ausdruck verwendet werden:
Wert = (Bedingung) ? (Ausdruck, wenn erfüllt) : (Ausdruck, wenn nicht erfüllt)
Optimierung
Eine der wesentlichsten Aufgaben bei der Spieleentwicklung ist die Optimierung. Je nachdem, wie umfangreich das Spiel ist, sind mehr oder weniger Optimierungen notwendig. Dabei muss nicht nur die Ausführungsgeschwindigkeit optimiert werden, sondern zum Beispiel auch der Resourcenverbrauch. Es ist wichtig, ein Gefühl für Optimierungsmöglichkeiten zu entwickeln und daher werden in diesem Tutorial auch immer auf solche hingewiesen.
In unserem Beispiel werden 64 Rechtecke gezeichnet. Mit einem einfachen Trick kann man die Anzahl der zu zeichnenden Rechtecke auf 33 reduzieren: Zuerst wird die gesamte Fläche mit der Farbe für die weißen Felder gefüllt und anschließend werden nur noch die Flächen der schwarzen Felder übermalt. Die Funktion paint_chess_board() sieht dann so aus:
function paint_chess_board() { var w = canvas.width / fields; var h = canvas.height / fields; // komplettes Feld mit weißer Farbe füllen context.fillStyle = white_color; context.fillRect( 0, 0, canvas.width, canvas.height ); // schwarze Farbe setzen context.fillStyle = black_color; for( var y = 0; y < fields; y++ ) { for( var x = 0; x < fields; x++ ) { // nur ungerade Felder zeichnen if( (x+y)&1 ) { context.fillRect( w * x, h * y, w, h ); } } } }
Spielfeld als Grafik laden
Eine weitere Optimierungsmöglichkeit besteht darin, eine fertige Grafik zu laden und anzuzeigen. Das hat den Vorteil, dass man so grafisch ansprechende Spielfelder verwenden kann und man spart sich auch eine Menge Programmierarbeit. Zur Vereinfachung habe ich das erzeugte Bild abgespeichert und auf meinem Server bereitgestellt.
Um ein Bild zu laden, ist es am einfachsten, dieses im HTML-Code einzufügen und unsichtbar zu machen:
<img id="chessboard" src="http://xtainment.net/HTML5/img/chessboard.png" style="visibility:hidden;">
Dieses Bild kann nun im JavaScript direkt verwendet werden:
var chessboard = document.getElementById("chessboard");
Mit einem weiteren Befehl lässt sich dieses Bild nun direkt in den Canvas kopieren:
context.drawImage( chessboard, 0, 0 );
Der gesamte Code der Seite sieht dann folgendermaßen aus:
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <head> <meta http-equiv="content-type" content="text/html;charset=UTF-8"> <title>Sample</title> <script type="text/javascript"> var canvas; var context; var chessboard; function init() { canvas = document.getElementById("board"); context = canvas.getContext("2d"); chessboard = document.getElementById("chessboard"); paint_chess_board(); } function paint_chess_board() { context.drawImage( chessboard, 0, 0 ); } </script> </head> <body onLoad="init();"> <canvas id="board" width="320" height="320" style="border:1px black solid">Dieser Browser ist nicht geeignet.</canvas> <img id="chessboard" src="http://xtainment.net/HTML5/img/chessboard.png" style="visibility:hidden;"> </body> </html>
- Zum Inhaltsverzeichnis Spieleentwicklung mit JavaScript - Einleitung
- Zurück zu Spieleentwicklung mit JavaScript - Grundlagen
- Weiter zu Spieleentwicklung mit JavaScript - Dynamische Spielfelder