Spieleentwicklung mit JavaScript - Die PlayField-Klasse
Inhaltsverzeichnis |
Die PlayField-Klasse
Inzwischen ist das JavaScript-Programm ein bißchen unübersichtlich geworden. Funktionen mischen sich mit Variablen und Eventhandlern. Bevor wir dieses Chaos erweitern, sollten wir erst ein bißchen aufräumen. Es bietet sich an, die Funktionen, die zur Darstellung des Spielfelds notwendig sind zusammenzufassen.
Exkurs: Klassen in JavaScript
Hier kommt die "objektorientierte Programmierung" (kurz OOP) ins Spiel, die es ermöglicht, zusammengehörende Daten und Funktionen sinnvoll zu strukturieren. JavaScript ist zwar keine objektorientierte Sprache, sie bietet aber ein paar Sprachkonstrukte, die es ermöglichen, einige Ansätze der Objektorientierung umzusetzen.
Ein Beispiel:
function Position( x, y ) { this.x = x; this.y = y; } Position.prototype.getX = function() { return this.x; } Position.prototype.getY = function() { return this.y; } Position.prototype.setXY = function( x, y ) { this.x = x; this.y = y; } var pos = new Position( 10, 20 ); alert( "x: " + pos.getX() + ", y: " + pos.getY() ); pos.setXY( 99, 100 ); alert( "x: " + pos.getX() + ", y: " + pos.getY() );
Die Klassendeklaration sieht aus wie eine ganz normale Funktion. Innerhalb dieser Funktion können Member-Variablen definiert und gleichzeitig initialisiert werden. Die Klassendeklaration dient somit auch gleichzeitig als Konstruktor. Ein Konstruktor einer Klasse ist eine Funktion, die direkt beim Erstellen aufgerufen wird und das Objekt "konstruiert", d.h. alle Member-Variablen initialisiert und andere Initialisierungen durchführt, so dass das Objekt benutzt werden kann.
In diesem Fall erhält die Klasse im Konstruktor x und y, die in den Membervariablen gespeichert werden.
Für die Definition der Methoden gibt es drei unterschiedliche Varianten. Ich empfehle die oben verwendete, da sie am übersichtlichsten ist. Die Alternativen wären, die Methoden direkt im Konstruktor zu definieren oder sie im JSN-Format anzugeben, was ich nur für sehr kleine Klassen empfehle.
Vorüberlegungen
Nun muss entschieden werden, welche Informationen in die PlayField-Klasse aufgenommen werden sollen:
- Die Referenzen auf die HTML-Elemente, die für die Darstellung des Spielfelds notwendig sind
- Alle Variablen, die dynamisch berechnet werden
- Die Tiles, die zum Zeichnen benötigt werden
- Die aktuelle Position des sichtbaren Bereichs
Außerdem wird die drawPlayField-Funktion in die Klasse aufgenommen.
Die Eventhandler und Steuerung wird nicht in die PlayField-Klasse integriert, da sich die Art der Steuerung von Spiel zu Spiel unterscheiden kann. Daher beschränken wir uns bei der Klasse auf die reine Darstellung behalten uns vor, für die Steuerung eigene Klassen zu erstellen, die man dann beliebig mit der PlayField-Klasse kombinieren kann.
Alle statischen Informationen wie die Anzahl der Tiles im Tileset und die Leveldefinition wird im Konstruktor übergeben.
Der PlayField-Konstruktor
function PlayField( playfield_id, tileset_id, tile_count_x, tile_count_y, level ) { this.canvas = document.getElementById( playfield_id ); this.context = this.canvas.getContext("2d"); var imgTileSet = document.getElementById( tileset_id ); this.tiles = new Array(); this.context.drawImage( imgTileSet, 0, 0 ); this.level = level; this.display_pixel_width = this.canvas.width; this.display_pixel_height = this.canvas.height; this.tile_pixel_width = imgTileSet.width / tile_count_x; this.tile_pixel_height = imgTileSet.height / tile_count_y; this.display_tile_width = Math.ceil( this.display_pixel_width / this.tile_pixel_width )+1; this.display_tile_height = Math.ceil( this.display_pixel_height / this.tile_pixel_height )+1; this.map_tile_width = level[0].length; this.map_tile_height = level.length; this.map_pixel_width = this.map_tile_width * this.tile_pixel_width; this.map_pixel_height = this.map_tile_height * this.tile_pixel_height; this.posx = 0; this.posy = 0; 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 = this.context.getImageData( x * this.tile_pixel_width, y * this.tile_pixel_height, this.tile_pixel_width, this.tile_pixel_height ); this.tiles[i] = document.getElementById("tile"+i); this.tiles[i].width = this.tile_pixel_width; this.tiles[i].height = this.tile_pixel_height; this.tiles[i].getContext("2d").putImageData( imgData, 0, 0 ); } } }
Der für das PlayField relevatnte Code aus der Init()-Funktion wurde nun in den Konstruktor übertragen. Alle Variablen-Zugriffe müssen nun mit this.<variable> angegeben werden.
Der Konstruktor erhält nun folgende Parameter:
playfield_id | Element-ID des Canvas, in dem das Spielfeld dargstellt werden soll. Die Größe des sichtbaren Bereichs wird den Attributen width und height entnommen. |
tileset_id | Element-ID des Images, das die Tileset-Grafik enthält. |
tile_count_x | Anzahl der Tiles in X-Richtung, die in der Tileset-Grafik enthalten sind. |
tile_count_y | Anzahl der Tiles in Y-Richtung, die in der Tileset-Grafik enthalten sind. |
level | Ein zweidimensionales Array mit den Offsets für die Tiles, welches das Level beschreibt. |
Die drawPlayField-Methode
Die Zeichenfunktion wurde nun so angeändert, dass sie keine Parameter mehr erhält. Sie entnimmt die benötigten Informationen den aktuellen Membervariablen, die dann mit anderen Methoden gesetzt werden können:
PlayField.prototype.drawPlayField = function() { var start_tile_x = Math.floor( this.posx / this.tile_pixel_width ); var start_tile_y = Math.floor( this.posy / this.tile_pixel_height ); var start_x = -(this.posx % this.tile_pixel_width); var start_y = -(this.posy % this.tile_pixel_height); for( y = 0; y <= this.display_tile_height; y++ ) { var ty = (start_tile_y + y) % this.map_tile_height; var py = start_y + y * this.tile_pixel_height; for( x = 0; x <= this.display_tile_width; x++ ) { var tx = (start_tile_x + x) % this.map_tile_width; var px = start_x + x * this.tile_pixel_width; this.context.drawImage( this.tiles[this.level[ty][tx]], px, py ); } } }
Bewegen der Karte
Für das Bewegen des sichtbaren Ausschnitts gibt es nun zwei Methoden move() und moveTo(). move() verschiebt die Position um die angegebene Distanz während moveTo() die absolute Position erhält. move() ruft berechnet die absolute Position und bedient sich der Methode moveTo(), um die Position entgültig zu setzen:
PlayField.prototype.moveTo = function( xpos, ypos ) { while( xpos < 0 ) xpos += this.map_pixel_width; while( ypos < 0 ) ypos += this.map_pixel_height; this.posx = xpos; this.posy = ypos; this.drawPlayField(); } PlayField.prototype.move = function( xdelta, ydelta ) { this.moveTo( this.posx + xdelta, this.posy + ydelta ); }
moveTo() stellt sicher, dass es ich bei den Position um positive Werte handelt, überträgt diese in die Membervariablen und ruft dann drawPlayField() auf, um die Karte an der gewünschten Position darzustellen.
Diese Methoden können dann von der Steuerung benutzt werden, um auf verschiedene Art und Weisen zu ermöglichen, durch das Spielfeld zu navigieren.
Positionsberechnungen
Zusätzlich werden noch eine Methode angeboten, die die aktuelle Position des sichtbaren Ausschnitts zurück liefert und eine andere, die aufgrund der Pixel-Position innerhalb des gesamten Spielfeldes die Position innerhalb des Level-Arrays berechnet. Letztere Methode ist zum Beispiel für einen Level-Editor nützlich.
PlayField.prototype.getPos = function() { return {x: this.posx, y: this.posy}; } PlayField.prototype.getTilePos = function( xpos, ypos ) { return { x: Math.floor( xpos / this.tile_pixel_width ) % this.map_tile_width, y: Math.floor( ypos / this.tile_pixel_height ) % this.map_tile_height }; }
Das Ergebnis beider Methoden ist ein namenloses Objekt, das die x- und y-Positionen enthalten.
var pos = pf.getPos(); alert( "x: " + pos.x + ", y: " + pos.y );
PlayField.js
Die komplette Klasse wird in eine separate Datei ausgelagert, die den Namen der Klasse haben sollte:
function PlayField( playfield_id, tileset_id, tile_count_x, tile_count_y, level ) { this.canvas = document.getElementById( playfield_id ); this.context = this.canvas.getContext("2d"); var imgTileSet = document.getElementById( tileset_id ); this.tiles = new Array(); this.context.drawImage( imgTileSet, 0, 0 ); this.level = level; this.display_pixel_width = this.canvas.width; this.display_pixel_height = this.canvas.height; this.tile_pixel_width = imgTileSet.width / tile_count_x; this.tile_pixel_height = imgTileSet.height / tile_count_y; this.display_tile_width = Math.ceil( this.display_pixel_width / this.tile_pixel_width )+1; this.display_tile_height = Math.ceil( this.display_pixel_height / this.tile_pixel_height )+1; this.map_tile_width = level[0].length; this.map_tile_height = level.length; this.map_pixel_width = this.map_tile_width * this.tile_pixel_width; this.map_pixel_height = this.map_tile_height * this.tile_pixel_height; this.posx = 0; this.posy = 0; 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 = this.context.getImageData( x * this.tile_pixel_width, y * this.tile_pixel_height, this.tile_pixel_width, this.tile_pixel_height ); this.tiles[i] = document.getElementById("tile"+i); this.tiles[i].width = this.tile_pixel_width; this.tiles[i].height = this.tile_pixel_height; this.tiles[i].getContext("2d").putImageData( imgData, 0, 0 ); } } } PlayField.prototype.moveTo = function( xpos, ypos ) { while( xpos < 0 ) xpos += this.map_pixel_width; while( ypos < 0 ) ypos += this.map_pixel_height; this.posx = xpos; this.posy = ypos; this.drawPlayField(); } PlayField.prototype.move = function( xdelta, ydelta ) { this.moveTo( this.posx + xdelta, this.posy + ydelta ); } PlayField.prototype.drawPlayField = function() { var start_tile_x = Math.floor( this.posx / this.tile_pixel_width ); var start_tile_y = Math.floor( this.posy / this.tile_pixel_height ); var start_x = -(this.posx % this.tile_pixel_width); var start_y = -(this.posy % this.tile_pixel_height); for( y = 0; y <= this.display_tile_height; y++ ) { var ty = (start_tile_y + y) % this.map_tile_height; var py = start_y + y * this.tile_pixel_height; for( x = 0; x <= this.display_tile_width; x++ ) { var tx = (start_tile_x + x) % this.map_tile_width; var px = start_x + x * this.tile_pixel_width; this.context.drawImage( this.tiles[this.level[ty][tx]], px, py ); } } } PlayField.prototype.getPos = function() { return {x: this.posx, y: this.posy }; } PlayField.prototype.getTilePos = function( xpos, ypos ) { return { x: Math.floor( xpos / this.tile_pixel_width ) % this.map_tile_width, y: Math.floor( ypos / this.tile_pixel_height ) % this.map_tile_height }; }
Anwendung der Klasse
Das Programm sieht nun wesentlich übersichtlicher aus, besteht im wesentlichen nur noch aus der Initialisierung und den EventHandlern und der Level-Definition.
<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; } .debug { position: absolute; left: 10px; top: 400px; } </style> <script type="text/javascript" src="PlayField.js"></script> <script type="text/javascript"> 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 ) ); var pf; function init() { pf = new PlayField( "board", "tileset", 5, 1, level ); pf.moveTo( 0, 0 ); var div = document.getElementById("board_div"); div.addEventListener('mousedown', OnMouseDown, false); div.addEventListener('mousemove', OnMouseMove, false); div.addEventListener('mouseup', OnMouseUp, false); } var dragx = -1; var dragy = -1; function showXY( x, y ) { document.f.t.value = "x: " + x + ", y: " + y; } function OnMouseDown( e, fromMove ) { if( (e.which == 1 || fromMove) ) { dragx = e.pageX; dragy = e.pageY; } } function OnMouseMove( e ) { if( dragx > 0 && dragy > 0 ) { OnMouseUp( e, true ); OnMouseDown( e, true ); } var pos = pf.getPos(); var tilepos = pf.getTilePos( e.pageX - this.offsetLeft + pos.x, e.pageY - this.offsetTop + pos.y ); showXY( tilepos.x, tilepos.y ); } function OnMouseUp( e, fromMove ) { if( (e.which == 1 || fromMove) && dragx > 0 && dragy > 0 ) { pf.move( dragx - e.pageX, dragy - e.pageY ); dragx = -1; dragy = -1; } } </script> </head> <body onLoad="init();"> <div class="board frame" id="board_div"> <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> <form name="f" class="debug"> <textarea name="t" cols=80 rows=3></textarea> </form> </body> </html>
In diesem Beispielprogramm kann die Karte durch Drücken und Festhalten der linken Maustaste bewegt werden, wobei die Karte immer exakt der Maus folgt.
In der Funktion OnMouseMove() wird nun auch die aktuelle Position innerhalb des Level-Arrays ermittelt und in der Textbox ausgegeben.