Spieleentwicklung mit JavaScript - Scrollende Spielfelder
(Unterschied zwischen Versionen)
(Die Seite wurde neu angelegt: „= Scrollende Spielfelder = In vielen Spielen wie zum Beispiel Jump'n'Run- oder Strategiespielen wird nur ein Auschnitt des gesamten Spielfelds dargestellt. Der Sp…“) |
(→Das Ergebnis) |
||
(Der Versionsvergleich bezieht 6 dazwischenliegende Versionen mit ein.) | |||
Zeile 1: | Zeile 1: | ||
= Scrollende Spielfelder = | = Scrollende Spielfelder = | ||
In vielen Spielen wie zum Beispiel Jump'n'Run- oder Strategiespielen wird nur ein Auschnitt des gesamten Spielfelds dargestellt. Der Spieler kann auf unterschiedliche Art und Weise durch das Spielfeld navigieren, wobei jeweils nur der aktuell sichtbare Bildschirmausschnitt gezeichnet wird. | In vielen Spielen wie zum Beispiel Jump'n'Run- oder Strategiespielen wird nur ein Auschnitt des gesamten Spielfelds dargestellt. Der Spieler kann auf unterschiedliche Art und Weise durch das Spielfeld navigieren, wobei jeweils nur der aktuell sichtbare Bildschirmausschnitt gezeichnet wird. | ||
+ | |||
+ | [[Datei:Scroll1.png]] | ||
+ | |||
+ | Weil sich das Spielfeld aus Tiles zusammensetzt, deren Größe sich von Spiel zu Spiel ändern kann, ist eine indirekte Berechnung der Spielfeldgröße notwendig: | ||
+ | Spielfeld-Breite = Anzahl der Tiles * Breite der Tiles. | ||
+ | Spielfeld-Höhe = Anzahl der Tiles * Höhe der Tiles. | ||
+ | |||
+ | Es ist sinnvoll, die Größe und Position des sichtbaren Bildschirmausschnitts in Pixel anzugeben und nicht in Tiles. Das hat den Vorteil, dass ein "Softscrolling" möglich ist, also eine Bewegung des sichtbaren Ausschnitts um einzelne Pixel. Würde man nur die Tile-Position angeben, würde das Bild immer um die Größe eines ganzen Tiles springen und das würde wirklich nicht gut aussehen. | ||
+ | |||
+ | Allerdings muss auch berechnet werden, welche Tiles im sichtbaren Bereich gezeichnet werden müssen. Es sind also eine ganze Reihe von Größenangaben und Berechnungen notwendig. Damit der Algorithmus für das scrollende Spielfeld möglichst flexibel eingesetzt werden kann, | ||
+ | werden wir soviele Werte wie möglich dynamisch errechnen. Das hat den Vorteil, dass später Änderungen und Anpassungen sehr einfach vorgenommen werden können. So könnte sich zum Beispiel die Größe der einzelnen Tiles ändern, die Kartengröße von Level zu Level variieren oder sich die Größe des sichtbaren Bildausschnitts an die Größe des Browserfensters anpassen. | ||
+ | |||
+ | == Ermittlung der Tiles-Größe == | ||
+ | |||
+ | Von folgendem Tileset können wir sagen, dass sich insgesamt 5 Tiles darin befinden. Wenn wir die Größe des Bildes kennen, können wir daraus leicht die Größe der einzelnen Tiles berechnen. Wenn man gleich vorsieht, dass die Tiles nicht unbedingt nebeneinander, sondern auch übereinander plaziert werden können, ist es lediglich notwendig, die Anzahl der Tiles jeweils in X- und Y-Richtung zu kennen: | ||
+ | |||
+ | [[Datei:Tileset.png]] | ||
+ | |||
+ | <pre> | ||
+ | var tile_count_x = 5; | ||
+ | var tile_count_y = 1; | ||
+ | </pre> | ||
+ | |||
+ | Die Breite und Höhe des Bildes, in der die Tiles enthalten sind, können wir dem Image-Objekt entnehmen und es ergibt sich folgende | ||
+ | Berechnung: | ||
+ | |||
+ | <pre> | ||
+ | var tile_pixel_width = 0; | ||
+ | var tile_pixel_height = 0; | ||
+ | |||
+ | function init() | ||
+ | { | ||
+ | // ... | ||
+ | var tileset = document.getElementById("tileset"); | ||
+ | tile_pixel_width = tileset.width / tile_count_x; | ||
+ | tile_pixel_height = tileset.height / tile_count_y; | ||
+ | // ... | ||
+ | } | ||
+ | </pre> | ||
+ | |||
+ | Alle weiteren Berechnungen beziehen sich auf die ermittelten Werte für tile_pixel_width und tile_pixel_height. So besteht die Möglichkeit, die Größe der Tiles nachträglich zu ändern, ohne eine einzige Änderung am Programm vornehmen zu müssen. | ||
+ | |||
+ | == Canvas-Größe == | ||
+ | |||
+ | Die Breite und Höhe des sichtbaren Bereichs kann im Canvas festgelegt werden. Leider muss sie direkt als width="xx" height="yy" im <canvas>-Element festgelegt werden. Wird eine Größe im Style für das Canvas definiert führt das zu einer Skalierung und nicht zu einer Erhöhung der Pixelanzahl. Deshalb ist es leider nicht möglich, die Pixel-Dimensionen eines Canvas über ein Stylesheet zu ändern. | ||
+ | |||
+ | Dennoch ist es sinnvoll, die Größe des Canvas aus den Angaben im canvas-Tag zu entnehmen: | ||
+ | |||
+ | <pre> | ||
+ | var display_pixel_width = 0; | ||
+ | var display_pixel_height = 0; | ||
+ | var display_tile_width = 0; | ||
+ | var display_tile_height = 0; | ||
+ | |||
+ | function init() | ||
+ | { | ||
+ | //... | ||
+ | display_pixel_width = canvas.width; | ||
+ | display_pixel_height = canvas.height; | ||
+ | display_tile_width = Math.floor( display_pixel_width / tile_pixel_width )+1; | ||
+ | display_tile_height = Math.floor( display_pixel_height / tile_pixel_height )+1; | ||
+ | //... | ||
+ | } | ||
+ | </pre> | ||
+ | |||
+ | == Spielfeld-Größe == | ||
+ | Die Größe des gesamten Spielfelds hängt von der Level-Definition und der Tile-Größe ab. Die Dimensionen des Arrays lassen sich mit | ||
+ | dem length-Attribut ermitteln. Wir gehen von der Annahme aus, dass alle Zeilen gleich lang sind und ermitteln nur die Länge der ersten | ||
+ | Zeile: | ||
+ | <pre> | ||
+ | var map_tile_width = 0; | ||
+ | var map_tile_height = 0; | ||
+ | var map_pixel_width = 0; | ||
+ | var map_pixel_height = 0; | ||
+ | |||
+ | var level = new Array( | ||
+ | new Array( 0,4,0,0,0,0 ), | ||
+ | new Array( 2,1,1,1,1,1 ), | ||
+ | new Array( 2,0,3,0,0,0 ), | ||
+ | new Array( 2,0,3,0,0,0 ), | ||
+ | new Array( 2,0,3,2,1,1 ) | ||
+ | ); | ||
+ | |||
+ | function init() | ||
+ | { | ||
+ | //.. | ||
+ | map_tile_width = level[0].length; | ||
+ | map_tile_height = level.length; | ||
+ | map_pixel_width = map_tile_width * tile_pixel_width; | ||
+ | map_pixel_height = map_tile_height * tile_pixel_height; | ||
+ | //.. | ||
+ | } | ||
+ | </pre> | ||
+ | |||
+ | == Zeichnen des sichtbaren Bereichs == | ||
+ | Für das Darstellen des darzustellenden Spielfeldausschnitts sind ein paar Berechnungen notwendig. | ||
+ | # Die Position innerhalb der Leveldaten muss berechnet werden. Hierbei ist zu berücksichtigen, dass die Teile nicht vollständig innerhalb des Fensters sichtbar sind. | ||
+ | # Aufgrund der angegebenen Pixelposition muss berechnet werden, mit welchem Teil an welcher Position das Zeichnen starten soll. Die erste Position (x und/oder y) kann negativ sein. Der nicht sichbare Teil wird von den Zeichenfunktionen automatisch abgeschnitten. | ||
+ | # Es muss darauf geachtet werden, dass beim Zeichnen am Ende des Spielfelds nicht auf ungültige Bereiche in den Leveldaten zugegriffen wird. Zu diesem Zweck findet ein "wrapping" statt, dass die Tile-Koordinaten umbricht, so dass sie wieder bei 0 anfangen, | ||
+ | wenn das Ende erreicht ist. Auf diese Weise kann man unendlich in eine Richtung scrollen, wobei sich die Karte immer wiederholt. | ||
+ | |||
+ | <pre> | ||
+ | function drawPlayfield( level, xpos, ypos ) | ||
+ | { | ||
+ | var start_tile_x = Math.floor( xpos / tile_pixel_width ); | ||
+ | var start_tile_y = Math.floor( ypos / tile_pixel_height ); | ||
+ | var start_x = -(xpos % tile_pixel_width); | ||
+ | var start_y = -(ypos % tile_pixel_height); | ||
+ | |||
+ | for( y = 0; y <= display_tile_height; y++ ) | ||
+ | { | ||
+ | var ty = (start_tile_y + y) % map_tile_height; | ||
+ | var py = start_y + y * tile_pixel_height; | ||
+ | |||
+ | for( x = 0; x <= display_tile_width; x++ ) | ||
+ | { | ||
+ | var tx = (start_tile_x + x) % map_tile_width; | ||
+ | var px = start_x + x * tile_pixel_width; | ||
+ | |||
+ | context.drawImage( tiles[level[ty][tx]], px, py ); | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | </pre> | ||
+ | Zuerst wird die Startposition in den Leveldaten ermittelt. Diese wird durch eine Division der Pixel-Position durch die Tile-Größe erreicht, wobei der Nachkomma-Anteil abgeschnitten wird. Dadurch wird ein Tile solange gezeichnet, bis es vollständig aus dem Fenster | ||
+ | verschwunden ist. | ||
+ | |||
+ | Beispiel: | ||
+ | |||
+ | Die Pixel-Position 29,59 ergibt eine Tile-Position von 0,1. Es werden also schon die nur teilweise sichtbaren Tiles gezeichnet. | ||
+ | |||
+ | Um die Start Position für das erste zu zeichnende Tile zu ermitteln, muss der Modulo der ersten Division berechnet und negiert werden. | ||
+ | D.h. bei einer Pixel-Position von 29,59 muss an der Position -29,-29 mit dem Zeichnen begonnen werden, damit der untere rechte Pixel | ||
+ | des Tiles noch sichtbar ist. | ||
+ | |||
+ | Um etwaige Umbrüche in den Leveldaten zu berücksichtigen, wird für die fortlaufende Tile-Position der Modulo zu gesamten Größe berechnet. Hat das Level eine Größe von 50x50 Tiles, beginnt die Zählung durch den Modulo nach der Position 49 wieder bei 0 und der | ||
+ | Anfang der Karte wird ans Ende des Bildes gesetzt. | ||
+ | |||
+ | '''Hinweis''' | ||
+ | |||
+ | Im Vergleich zum vorhergenden Beispiel kann nicht mit putImageData() gearbeitet werden, weil diese Funktion im Firefox nicht vollständig implementiert ist. Deshalb werden die einzelnen Tiles in separate Canvas-Objekte gespeichert und können dann mit der | ||
+ | drawImage() Funktion gezeichnet werden, die sich selbst um das Clipping kümmert. | ||
+ | |||
+ | == Das Ergebnis == | ||
+ | <pre> | ||
+ | <html> | ||
+ | <head> | ||
+ | <meta http-equiv="content-type" content="text/html;charset=UTF-8"> | ||
+ | <title>Sample</title> | ||
+ | <style type="text/css"> | ||
+ | body | ||
+ | { | ||
+ | margin: 0px; | ||
+ | padding: 0px; | ||
+ | } | ||
+ | .hidden | ||
+ | { | ||
+ | visibility: hidden; | ||
+ | } | ||
+ | .frame | ||
+ | { | ||
+ | border:1px black solid; | ||
+ | } | ||
+ | .board | ||
+ | { | ||
+ | position: absolute; | ||
+ | left: 10px; | ||
+ | top: 10px; | ||
+ | } | ||
+ | </style> | ||
+ | <script type="text/javascript"> | ||
+ | var tile_count_x = 5; | ||
+ | var tile_count_y = 1; | ||
+ | |||
+ | var canvas; | ||
+ | var context; | ||
+ | var tiles = new Array(); | ||
+ | |||
+ | var display_pixel_width = 0; | ||
+ | var display_pixel_height = 0; | ||
+ | var display_tile_width = 0; | ||
+ | var display_tile_height = 0; | ||
+ | var tile_pixel_width = 0; | ||
+ | var tile_pixel_height = 0; | ||
+ | var map_tile_width = 0; | ||
+ | var map_tile_height = 0; | ||
+ | var map_pixel_width = 0; | ||
+ | var map_pixel_height = 0; | ||
+ | |||
+ | var level = new Array( | ||
+ | new Array( 0,4,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,4,0,0,0,0,0,0 ), | ||
+ | new Array( 2,1,1,1,1,1,1,2,2,1,1,1,1,1,1,2,2,1,1,1,1,1,1,2,2,1,1,1,1,1,1,2 ), | ||
+ | new Array( 2,0,3,0,0,0,0,2,2,0,3,0,0,0,0,2,2,0,3,0,0,0,0,2,2,0,3,0,0,0,0,2 ), | ||
+ | new Array( 2,0,3,0,0,0,0,2,2,0,3,0,0,0,0,2,2,0,3,0,0,0,0,2,2,0,3,0,0,0,0,2 ), | ||
+ | new Array( 2,0,3,2,1,1,1,2,2,0,3,2,1,1,1,2,2,0,3,2,1,1,1,2,2,0,3,2,1,1,1,2 ), | ||
+ | new Array( 2,4,3,2,0,0,0,4,2,4,3,2,0,0,0,4,2,4,3,2,0,0,0,4,2,4,3,2,0,0,0,4 ), | ||
+ | new Array( 1,1,1,2,1,0,1,1,1,1,1,2,1,0,1,1,1,1,1,2,1,0,1,1,1,1,1,2,1,0,1,1 ), | ||
+ | new Array( 4,0,0,2,0,0,4,0,4,0,0,2,0,0,4,0,4,0,0,2,0,0,4,0,4,0,0,2,0,0,4,0 ), | ||
+ | new Array( 0,4,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,4,0,0,0,0,0,0 ), | ||
+ | new Array( 2,1,1,1,1,1,1,2,2,1,1,1,1,1,1,2,2,1,1,1,1,1,1,2,2,1,1,1,1,1,1,2 ), | ||
+ | new Array( 2,0,3,0,0,0,0,2,2,0,3,0,0,0,0,2,2,0,3,0,0,0,0,2,2,0,3,0,0,0,0,2 ), | ||
+ | new Array( 2,0,3,0,0,0,0,2,2,0,3,0,0,0,0,2,2,0,3,0,0,0,0,2,2,0,3,0,0,0,0,2 ), | ||
+ | new Array( 2,0,3,2,1,1,1,2,2,0,3,2,1,1,1,2,2,0,3,2,1,1,1,2,2,0,3,2,1,1,1,2 ), | ||
+ | new Array( 2,4,3,2,0,0,0,4,2,4,3,2,0,0,0,4,2,4,3,2,0,0,0,4,2,4,3,2,0,0,0,4 ), | ||
+ | new Array( 1,1,1,2,1,0,1,1,1,1,1,2,1,0,1,1,1,1,1,2,1,0,1,1,1,1,1,2,1,0,1,1 ), | ||
+ | new Array( 4,0,0,2,0,0,4,0,4,0,0,2,0,0,4,0,4,0,0,2,0,0,4,0,4,0,0,2,0,0,4,0 ), | ||
+ | new Array( 0,4,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,4,0,0,0,0,0,0 ), | ||
+ | new Array( 2,1,1,1,1,1,1,2,2,1,1,1,1,1,1,2,2,1,1,1,1,1,1,2,2,1,1,1,1,1,1,2 ), | ||
+ | new Array( 2,0,3,0,0,0,0,2,2,0,3,0,0,0,0,2,2,0,3,0,0,0,0,2,2,0,3,0,0,0,0,2 ), | ||
+ | new Array( 2,0,3,0,0,0,0,2,2,0,3,0,0,0,0,2,2,0,3,0,0,0,0,2,2,0,3,0,0,0,0,2 ), | ||
+ | new Array( 2,0,3,2,1,1,1,2,2,0,3,2,1,1,1,2,2,0,3,2,1,1,1,2,2,0,3,2,1,1,1,2 ), | ||
+ | new Array( 2,4,3,2,0,0,0,4,2,4,3,2,0,0,0,4,2,4,3,2,0,0,0,4,2,4,3,2,0,0,0,4 ), | ||
+ | new Array( 1,1,1,2,1,0,1,1,1,1,1,2,1,0,1,1,1,1,1,2,1,0,1,1,1,1,1,2,1,0,1,1 ), | ||
+ | new Array( 4,0,0,2,0,0,4,0,4,0,0,2,0,0,4,0,4,0,0,2,0,0,4,0,4,0,0,2,0,0,4,0 ) | ||
+ | ); | ||
+ | |||
+ | function init() | ||
+ | { | ||
+ | canvas = document.getElementById("board"); | ||
+ | context = canvas.getContext("2d"); | ||
+ | |||
+ | var tileset = document.getElementById("tileset"); | ||
+ | context.drawImage( tileset, 0, 0 ); | ||
+ | |||
+ | display_pixel_width = canvas.width; | ||
+ | display_pixel_height = canvas.height; | ||
+ | tile_pixel_width = tileset.width / tile_count_x; | ||
+ | tile_pixel_height = tileset.height / tile_count_y; | ||
+ | |||
+ | //alert( "x: " + display_pixel_width + ", y: " + display_pixel_height ); | ||
+ | |||
+ | for( var y = 0; y < tile_count_y; y++ ) | ||
+ | { | ||
+ | for( var x = 0; x < tile_count_x; x++ ) | ||
+ | { | ||
+ | var i = y * tile_count_x + x; | ||
+ | var imgData = context.getImageData( x * tile_pixel_width, y * tile_pixel_height, tile_pixel_width, tile_pixel_height ); | ||
+ | tiles[i] = document.getElementById("tile"+i); | ||
+ | tiles[i].width = tile_pixel_width; | ||
+ | tiles[i].height = tile_pixel_height; | ||
+ | tiles[i].getContext("2d").putImageData( imgData, 0, 0 ); | ||
+ | } | ||
+ | } | ||
+ | |||
+ | display_tile_width = Math.ceil( display_pixel_width / tile_pixel_width )+1; | ||
+ | display_tile_height = Math.ceil( display_pixel_height / tile_pixel_height )+1; | ||
+ | map_tile_width = level[0].length; | ||
+ | map_tile_height = level.length; | ||
+ | map_pixel_width = map_tile_width * tile_pixel_width; | ||
+ | map_pixel_height = map_tile_height * tile_pixel_height; | ||
+ | |||
+ | scrollPlayfield(); | ||
+ | |||
+ | setInterval( "scrollPlayfield();", 50 ); | ||
+ | } | ||
+ | |||
+ | var pfx = 0; | ||
+ | var pfy = 0; | ||
+ | var xdir = -1; | ||
+ | var ydir = -1; | ||
+ | |||
+ | function scrollPlayfield() | ||
+ | { | ||
+ | while( pfx < 0 ) pfx += map_pixel_width; | ||
+ | while( pfy < 0 ) pfy += map_pixel_height; | ||
+ | |||
+ | drawPlayfield( level, pfx, pfy ); | ||
+ | showXY( pfx, pfy ); | ||
+ | |||
+ | pfx += xdir; | ||
+ | pfy += ydir; | ||
+ | } | ||
+ | |||
+ | function drawPlayfield( level, xpos, ypos ) | ||
+ | { | ||
+ | var start_tile_x = Math.floor( xpos / tile_pixel_width ); | ||
+ | var start_tile_y = Math.floor( ypos / tile_pixel_height ); | ||
+ | var start_x = -(xpos % tile_pixel_width); | ||
+ | var start_y = -(ypos % tile_pixel_height); | ||
+ | |||
+ | for( y = 0; y <= display_tile_height; y++ ) | ||
+ | { | ||
+ | var ty = (start_tile_y + y) % map_tile_height; | ||
+ | var py = start_y + y * tile_pixel_height; | ||
+ | |||
+ | for( x = 0; x <= display_tile_width; x++ ) | ||
+ | { | ||
+ | var tx = (start_tile_x + x) % map_tile_width; | ||
+ | var px = start_x + x * tile_pixel_width; | ||
+ | |||
+ | context.drawImage( tiles[level[ty][tx]], px, py ); | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | </script> | ||
+ | </head> | ||
+ | <body onLoad="init();"> | ||
+ | <div class="board frame"><canvas id="board" width="460" height="340">Dieser Browser ist nicht geeignet.</canvas></div> | ||
+ | <img id="tileset" src="img/tileset.png" class="hidden"> | ||
+ | <canvas id="tile0" class="hidden" width="30" height="30"></canvas> | ||
+ | <canvas id="tile1" class="hidden" width="30" height="30"></canvas> | ||
+ | <canvas id="tile2" class="hidden" width="30" height="30"></canvas> | ||
+ | <canvas id="tile3" class="hidden" width="30" height="30"></canvas> | ||
+ | <canvas id="tile4" class="hidden" width="30" height="30"></canvas> | ||
+ | </body> | ||
+ | </html> | ||
+ | </pre> | ||
+ | Da wir noch keine Steuerungsmöglichkeit haben, wird eine Funktion scrollPlayfield() zyklisch aufgerufen (setInterval()), die die Karte jeweils um einen Pixel verschiebt, so dass sich der sichtbare Bereich langsam über die Karte bewegt. | ||
+ | |||
+ | Es ist zu beachten, dass die Position, die der Funktion drawPlayfield() übergeben wird, nicht negativ werden darf. Aus diesem Grund wird in der scrollPlayfield() immer geprüft, ob eine der beiden Parameter negativ ist und solange um die Größe der Karte erhöht, bis sie positiv ist. Das funktioniert aufgrund der Modulo-Berechnung der Position innerhalb der Tiles ohne Probleme. | ||
+ | |||
+ | * Zum Inhalt [[Spieleentwicklung mit JavaScript - Einleitung]] | ||
+ | * Zurück zu [[Spieleentwicklung mit JavaScript - Dynamische Spielfelder]] | ||
+ | * Weiter zu [[Spieleentwicklung mit JavaScript - Maus-Steuerung]] |
Aktuelle Version vom 15:42, 2. Sep. 2010
Inhaltsverzeichnis |
Scrollende Spielfelder
In vielen Spielen wie zum Beispiel Jump'n'Run- oder Strategiespielen wird nur ein Auschnitt des gesamten Spielfelds dargestellt. Der Spieler kann auf unterschiedliche Art und Weise durch das Spielfeld navigieren, wobei jeweils nur der aktuell sichtbare Bildschirmausschnitt gezeichnet wird.
Weil sich das Spielfeld aus Tiles zusammensetzt, deren Größe sich von Spiel zu Spiel ändern kann, ist eine indirekte Berechnung der Spielfeldgröße notwendig: Spielfeld-Breite = Anzahl der Tiles * Breite der Tiles. Spielfeld-Höhe = Anzahl der Tiles * Höhe der Tiles.
Es ist sinnvoll, die Größe und Position des sichtbaren Bildschirmausschnitts in Pixel anzugeben und nicht in Tiles. Das hat den Vorteil, dass ein "Softscrolling" möglich ist, also eine Bewegung des sichtbaren Ausschnitts um einzelne Pixel. Würde man nur die Tile-Position angeben, würde das Bild immer um die Größe eines ganzen Tiles springen und das würde wirklich nicht gut aussehen.
Allerdings muss auch berechnet werden, welche Tiles im sichtbaren Bereich gezeichnet werden müssen. Es sind also eine ganze Reihe von Größenangaben und Berechnungen notwendig. Damit der Algorithmus für das scrollende Spielfeld möglichst flexibel eingesetzt werden kann, werden wir soviele Werte wie möglich dynamisch errechnen. Das hat den Vorteil, dass später Änderungen und Anpassungen sehr einfach vorgenommen werden können. So könnte sich zum Beispiel die Größe der einzelnen Tiles ändern, die Kartengröße von Level zu Level variieren oder sich die Größe des sichtbaren Bildausschnitts an die Größe des Browserfensters anpassen.
Ermittlung der Tiles-Größe
Von folgendem Tileset können wir sagen, dass sich insgesamt 5 Tiles darin befinden. Wenn wir die Größe des Bildes kennen, können wir daraus leicht die Größe der einzelnen Tiles berechnen. Wenn man gleich vorsieht, dass die Tiles nicht unbedingt nebeneinander, sondern auch übereinander plaziert werden können, ist es lediglich notwendig, die Anzahl der Tiles jeweils in X- und Y-Richtung zu kennen:
var tile_count_x = 5; var tile_count_y = 1;
Die Breite und Höhe des Bildes, in der die Tiles enthalten sind, können wir dem Image-Objekt entnehmen und es ergibt sich folgende Berechnung:
var tile_pixel_width = 0; var tile_pixel_height = 0; function init() { // ... var tileset = document.getElementById("tileset"); tile_pixel_width = tileset.width / tile_count_x; tile_pixel_height = tileset.height / tile_count_y; // ... }
Alle weiteren Berechnungen beziehen sich auf die ermittelten Werte für tile_pixel_width und tile_pixel_height. So besteht die Möglichkeit, die Größe der Tiles nachträglich zu ändern, ohne eine einzige Änderung am Programm vornehmen zu müssen.
Canvas-Größe
Die Breite und Höhe des sichtbaren Bereichs kann im Canvas festgelegt werden. Leider muss sie direkt als width="xx" height="yy" im <canvas>-Element festgelegt werden. Wird eine Größe im Style für das Canvas definiert führt das zu einer Skalierung und nicht zu einer Erhöhung der Pixelanzahl. Deshalb ist es leider nicht möglich, die Pixel-Dimensionen eines Canvas über ein Stylesheet zu ändern.
Dennoch ist es sinnvoll, die Größe des Canvas aus den Angaben im canvas-Tag zu entnehmen:
var display_pixel_width = 0; var display_pixel_height = 0; var display_tile_width = 0; var display_tile_height = 0; function init() { //... display_pixel_width = canvas.width; display_pixel_height = canvas.height; display_tile_width = Math.floor( display_pixel_width / tile_pixel_width )+1; display_tile_height = Math.floor( display_pixel_height / tile_pixel_height )+1; //... }
Spielfeld-Größe
Die Größe des gesamten Spielfelds hängt von der Level-Definition und der Tile-Größe ab. Die Dimensionen des Arrays lassen sich mit dem length-Attribut ermitteln. Wir gehen von der Annahme aus, dass alle Zeilen gleich lang sind und ermitteln nur die Länge der ersten Zeile:
var map_tile_width = 0; var map_tile_height = 0; var map_pixel_width = 0; var map_pixel_height = 0; var level = new Array( new Array( 0,4,0,0,0,0 ), new Array( 2,1,1,1,1,1 ), new Array( 2,0,3,0,0,0 ), new Array( 2,0,3,0,0,0 ), new Array( 2,0,3,2,1,1 ) ); function init() { //.. map_tile_width = level[0].length; map_tile_height = level.length; map_pixel_width = map_tile_width * tile_pixel_width; map_pixel_height = map_tile_height * tile_pixel_height; //.. }
Zeichnen des sichtbaren Bereichs
Für das Darstellen des darzustellenden Spielfeldausschnitts sind ein paar Berechnungen notwendig.
- Die Position innerhalb der Leveldaten muss berechnet werden. Hierbei ist zu berücksichtigen, dass die Teile nicht vollständig innerhalb des Fensters sichtbar sind.
- Aufgrund der angegebenen Pixelposition muss berechnet werden, mit welchem Teil an welcher Position das Zeichnen starten soll. Die erste Position (x und/oder y) kann negativ sein. Der nicht sichbare Teil wird von den Zeichenfunktionen automatisch abgeschnitten.
- Es muss darauf geachtet werden, dass beim Zeichnen am Ende des Spielfelds nicht auf ungültige Bereiche in den Leveldaten zugegriffen wird. Zu diesem Zweck findet ein "wrapping" statt, dass die Tile-Koordinaten umbricht, so dass sie wieder bei 0 anfangen,
wenn das Ende erreicht ist. Auf diese Weise kann man unendlich in eine Richtung scrollen, wobei sich die Karte immer wiederholt.
function drawPlayfield( level, xpos, ypos ) { var start_tile_x = Math.floor( xpos / tile_pixel_width ); var start_tile_y = Math.floor( ypos / tile_pixel_height ); var start_x = -(xpos % tile_pixel_width); var start_y = -(ypos % tile_pixel_height); for( y = 0; y <= display_tile_height; y++ ) { var ty = (start_tile_y + y) % map_tile_height; var py = start_y + y * tile_pixel_height; for( x = 0; x <= display_tile_width; x++ ) { var tx = (start_tile_x + x) % map_tile_width; var px = start_x + x * tile_pixel_width; context.drawImage( tiles[level[ty][tx]], px, py ); } } }
Zuerst wird die Startposition in den Leveldaten ermittelt. Diese wird durch eine Division der Pixel-Position durch die Tile-Größe erreicht, wobei der Nachkomma-Anteil abgeschnitten wird. Dadurch wird ein Tile solange gezeichnet, bis es vollständig aus dem Fenster verschwunden ist.
Beispiel:
Die Pixel-Position 29,59 ergibt eine Tile-Position von 0,1. Es werden also schon die nur teilweise sichtbaren Tiles gezeichnet.
Um die Start Position für das erste zu zeichnende Tile zu ermitteln, muss der Modulo der ersten Division berechnet und negiert werden. D.h. bei einer Pixel-Position von 29,59 muss an der Position -29,-29 mit dem Zeichnen begonnen werden, damit der untere rechte Pixel des Tiles noch sichtbar ist.
Um etwaige Umbrüche in den Leveldaten zu berücksichtigen, wird für die fortlaufende Tile-Position der Modulo zu gesamten Größe berechnet. Hat das Level eine Größe von 50x50 Tiles, beginnt die Zählung durch den Modulo nach der Position 49 wieder bei 0 und der Anfang der Karte wird ans Ende des Bildes gesetzt.
Hinweis
Im Vergleich zum vorhergenden Beispiel kann nicht mit putImageData() gearbeitet werden, weil diese Funktion im Firefox nicht vollständig implementiert ist. Deshalb werden die einzelnen Tiles in separate Canvas-Objekte gespeichert und können dann mit der drawImage() Funktion gezeichnet werden, die sich selbst um das Clipping kümmert.
Das Ergebnis
<html> <head> <meta http-equiv="content-type" content="text/html;charset=UTF-8"> <title>Sample</title> <style type="text/css"> body { margin: 0px; padding: 0px; } .hidden { visibility: hidden; } .frame { border:1px black solid; } .board { position: absolute; left: 10px; top: 10px; } </style> <script type="text/javascript"> var tile_count_x = 5; var tile_count_y = 1; var canvas; var context; var tiles = new Array(); var display_pixel_width = 0; var display_pixel_height = 0; var display_tile_width = 0; var display_tile_height = 0; var tile_pixel_width = 0; var tile_pixel_height = 0; var map_tile_width = 0; var map_tile_height = 0; var map_pixel_width = 0; var map_pixel_height = 0; var level = new Array( new Array( 0,4,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,4,0,0,0,0,0,0 ), new Array( 2,1,1,1,1,1,1,2,2,1,1,1,1,1,1,2,2,1,1,1,1,1,1,2,2,1,1,1,1,1,1,2 ), new Array( 2,0,3,0,0,0,0,2,2,0,3,0,0,0,0,2,2,0,3,0,0,0,0,2,2,0,3,0,0,0,0,2 ), new Array( 2,0,3,0,0,0,0,2,2,0,3,0,0,0,0,2,2,0,3,0,0,0,0,2,2,0,3,0,0,0,0,2 ), new Array( 2,0,3,2,1,1,1,2,2,0,3,2,1,1,1,2,2,0,3,2,1,1,1,2,2,0,3,2,1,1,1,2 ), new Array( 2,4,3,2,0,0,0,4,2,4,3,2,0,0,0,4,2,4,3,2,0,0,0,4,2,4,3,2,0,0,0,4 ), new Array( 1,1,1,2,1,0,1,1,1,1,1,2,1,0,1,1,1,1,1,2,1,0,1,1,1,1,1,2,1,0,1,1 ), new Array( 4,0,0,2,0,0,4,0,4,0,0,2,0,0,4,0,4,0,0,2,0,0,4,0,4,0,0,2,0,0,4,0 ), new Array( 0,4,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,4,0,0,0,0,0,0 ), new Array( 2,1,1,1,1,1,1,2,2,1,1,1,1,1,1,2,2,1,1,1,1,1,1,2,2,1,1,1,1,1,1,2 ), new Array( 2,0,3,0,0,0,0,2,2,0,3,0,0,0,0,2,2,0,3,0,0,0,0,2,2,0,3,0,0,0,0,2 ), new Array( 2,0,3,0,0,0,0,2,2,0,3,0,0,0,0,2,2,0,3,0,0,0,0,2,2,0,3,0,0,0,0,2 ), new Array( 2,0,3,2,1,1,1,2,2,0,3,2,1,1,1,2,2,0,3,2,1,1,1,2,2,0,3,2,1,1,1,2 ), new Array( 2,4,3,2,0,0,0,4,2,4,3,2,0,0,0,4,2,4,3,2,0,0,0,4,2,4,3,2,0,0,0,4 ), new Array( 1,1,1,2,1,0,1,1,1,1,1,2,1,0,1,1,1,1,1,2,1,0,1,1,1,1,1,2,1,0,1,1 ), new Array( 4,0,0,2,0,0,4,0,4,0,0,2,0,0,4,0,4,0,0,2,0,0,4,0,4,0,0,2,0,0,4,0 ), new Array( 0,4,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,4,0,0,0,0,0,0 ), new Array( 2,1,1,1,1,1,1,2,2,1,1,1,1,1,1,2,2,1,1,1,1,1,1,2,2,1,1,1,1,1,1,2 ), new Array( 2,0,3,0,0,0,0,2,2,0,3,0,0,0,0,2,2,0,3,0,0,0,0,2,2,0,3,0,0,0,0,2 ), new Array( 2,0,3,0,0,0,0,2,2,0,3,0,0,0,0,2,2,0,3,0,0,0,0,2,2,0,3,0,0,0,0,2 ), new Array( 2,0,3,2,1,1,1,2,2,0,3,2,1,1,1,2,2,0,3,2,1,1,1,2,2,0,3,2,1,1,1,2 ), new Array( 2,4,3,2,0,0,0,4,2,4,3,2,0,0,0,4,2,4,3,2,0,0,0,4,2,4,3,2,0,0,0,4 ), new Array( 1,1,1,2,1,0,1,1,1,1,1,2,1,0,1,1,1,1,1,2,1,0,1,1,1,1,1,2,1,0,1,1 ), new Array( 4,0,0,2,0,0,4,0,4,0,0,2,0,0,4,0,4,0,0,2,0,0,4,0,4,0,0,2,0,0,4,0 ) ); function init() { canvas = document.getElementById("board"); context = canvas.getContext("2d"); var tileset = document.getElementById("tileset"); context.drawImage( tileset, 0, 0 ); display_pixel_width = canvas.width; display_pixel_height = canvas.height; tile_pixel_width = tileset.width / tile_count_x; tile_pixel_height = tileset.height / tile_count_y; //alert( "x: " + display_pixel_width + ", y: " + display_pixel_height ); for( var y = 0; y < tile_count_y; y++ ) { for( var x = 0; x < tile_count_x; x++ ) { var i = y * tile_count_x + x; var imgData = context.getImageData( x * tile_pixel_width, y * tile_pixel_height, tile_pixel_width, tile_pixel_height ); tiles[i] = document.getElementById("tile"+i); tiles[i].width = tile_pixel_width; tiles[i].height = tile_pixel_height; tiles[i].getContext("2d").putImageData( imgData, 0, 0 ); } } display_tile_width = Math.ceil( display_pixel_width / tile_pixel_width )+1; display_tile_height = Math.ceil( display_pixel_height / tile_pixel_height )+1; map_tile_width = level[0].length; map_tile_height = level.length; map_pixel_width = map_tile_width * tile_pixel_width; map_pixel_height = map_tile_height * tile_pixel_height; scrollPlayfield(); setInterval( "scrollPlayfield();", 50 ); } var pfx = 0; var pfy = 0; var xdir = -1; var ydir = -1; function scrollPlayfield() { while( pfx < 0 ) pfx += map_pixel_width; while( pfy < 0 ) pfy += map_pixel_height; drawPlayfield( level, pfx, pfy ); showXY( pfx, pfy ); pfx += xdir; pfy += ydir; } function drawPlayfield( level, xpos, ypos ) { var start_tile_x = Math.floor( xpos / tile_pixel_width ); var start_tile_y = Math.floor( ypos / tile_pixel_height ); var start_x = -(xpos % tile_pixel_width); var start_y = -(ypos % tile_pixel_height); for( y = 0; y <= display_tile_height; y++ ) { var ty = (start_tile_y + y) % map_tile_height; var py = start_y + y * tile_pixel_height; for( x = 0; x <= display_tile_width; x++ ) { var tx = (start_tile_x + x) % map_tile_width; var px = start_x + x * tile_pixel_width; context.drawImage( tiles[level[ty][tx]], px, py ); } } } </script> </head> <body onLoad="init();"> <div class="board frame"><canvas id="board" width="460" height="340">Dieser Browser ist nicht geeignet.</canvas></div> <img id="tileset" src="img/tileset.png" class="hidden"> <canvas id="tile0" class="hidden" width="30" height="30"></canvas> <canvas id="tile1" class="hidden" width="30" height="30"></canvas> <canvas id="tile2" class="hidden" width="30" height="30"></canvas> <canvas id="tile3" class="hidden" width="30" height="30"></canvas> <canvas id="tile4" class="hidden" width="30" height="30"></canvas> </body> </html>
Da wir noch keine Steuerungsmöglichkeit haben, wird eine Funktion scrollPlayfield() zyklisch aufgerufen (setInterval()), die die Karte jeweils um einen Pixel verschiebt, so dass sich der sichtbare Bereich langsam über die Karte bewegt.
Es ist zu beachten, dass die Position, die der Funktion drawPlayfield() übergeben wird, nicht negativ werden darf. Aus diesem Grund wird in der scrollPlayfield() immer geprüft, ob eine der beiden Parameter negativ ist und solange um die Größe der Karte erhöht, bis sie positiv ist. Das funktioniert aufgrund der Modulo-Berechnung der Position innerhalb der Tiles ohne Probleme.