... denn seine große Stunde kam,
immer wenn er Pillen nahm! -
Als ich noch vor einem Apple II+ saß, gehörte Pacman zu meinen Lieblingsspielen. Natürlich musste man irgendwie in die Interna hineinsehen, und siehe da: dessen Steuerung geschah nach der Methode: 20x Tastatur abfragen, dann 1x Pacman und Geister bewegen. Einige Jahre später und um einige Programmiererfahrung reicher bastelte ich mir mit Turbo-Pascal 6 meinen eigenen Pacman. Weil ich auch so ganz nebenher Aquarianer bin, wurde Pacman zu einem gefräßigen Barsch, der sich über Futtertabletten hermachte.
Dann reizte es mich, mit Javascript und HTML Sprites nachzubasteln. Auslöser war ein Schneeflockenprogramm, was mein damaliger Chef mir vorsetzte zum Anpassen an die hauseigene Web-Einstiegsseite. Und mein alter Pacfish schien mir gut geeignet dafür.
Nach der Methode "Gib dem Affen Zucker, aber bleibe dabei bei 24x24 Pix" entstanden diese Grafiken mit dPaint (ach ja, ca. 3 Jahre vor der Tp6-Variante gab es eine in Amigabasic, bei der Tunnels und Harpune entstanden).
Pacfish selbst muss in 4 Varianten erscheinen, für jede Himmelsrichtung eine. Zusätzlich müssen Bewegungsphasen eingeplant werden. Hierbei reicht es aber voll und ganz, nur den Paccie einmal mit geöffnetem Maul zu zeichnen und einmal mit geschlossenem Maul. Es bringt nichts, die Bewegung noch weiter aufzulösen in 4 Phasen: Mäuler bewegen sich nun mal blitzschnell.
Fazit: wir brauchen 8 Fische, von denen 2 gezeichnet werden müssen, die anderen erhält man durch Drehen und Spiegeln der 2 Ursprungszeichnungen:
Halt! Eine Phase haben wir vergessen: den toten Pacfish! Sind also insgesamt 9, die zweckmäßigerweise nach den Richtungen und Phasen benannt werden.
Im Original gab es 4 verschiedene Geister, also gibt es hier 4 verschiedene, blutdürstige Bastarde von Teufel und Krake, schön knallbunt. In der Amiga-Version schielten sie noch in die Laufrichtung, aber da sich das Datenaufkommen dafür vervierfachen würde, sieht die Internet-Version nur nach vorne. Auch waren die Amiga-Pixel mindestens 4x so groß, so dass die Augeneffekte gut sichtbar waren im Gegensatz zu den Pixeln auf den heutigen Monitoren.
Zusätzlich brauchen wir noch ein Bild für geschwächte Kraken, die sich hier dem Hintergrund anpassen, ein Bild für Kraken, die gefressen wurden, und von denen nur noch die Augen übrigblieben, zuletzt noch ein Bild für Kraken, die bald wieder ihre volle Stärke erreicht haben.
Diese Grafiken sind einfach ein kleiner und ein großer Kreis. Aber nicht nur diese können aufgenommen werden, sondern auch Pfeile. Für die Schuss-Darstellung braucht man letztere wieder für alle 4 Himmelsrichtungen.
Die erste Pacfish-Variante enthielt einen dem Meeresboden nachempfundenen Hintergrund. Bei 24x24 sah man leider immer einen unschönen Kacheleffekt, bei 540x360 wurde das Spiel dadurch so lahm, dass in der jetzigen Variante der Hintergrund ein transparentes, 1-Pixel großes Gif ist, dass vom HTML-Teil auf 24x24 Pixel "aufgebläht" wurde. Die Mauern oder besser: Hindernisse sind tangbewachsene Steine, der Tunnel ein schwarzes Loch und die Krakenstartpunkte ein glitzerndes Netz. Letztere müssen extra markiert werden, weil sie für den Paccie nicht passierbar sind.
Blieben noch die Goodies, die an dem Platz erscheinen, an dem der Paccie startet. Implementiert wurden:
Später gab es noch fürs Feld den "Bremser" und den "Beschleuniger" u.ä..
Damit sind erst mal alle im Javascript-Paccie verwendeten Bilder erstellt. Nimmt man nich den Pfeil dazu, der beim Leveleditor das gerade aktive Element anzeigt, sind die Vorarbeiten mit einem Grafikprogramm abgeschlossen.
zum Seitenanfang | ||
zum Seitenende |
Ein Level-Editor ist nichts weiter als eine Seite, in der man vorhandene Elemente in die Tabellenbildchen ablegt und auf Wunsch den Code für die Feldbelegung des Spielfeldes bekommt. Das heisst:
Dieser spezielle Level-Editor für Pacfish bestimmt zusätzlich die möglichen Bewegungen von Paccie und den Kraken und packt das Ergebnis in eine Lookup-Tabelle mit der gleichen Größe des Spielfeldes, damit das Hauptprogramm so wenig wie möglich mit Rechenarbeit belastet wird. Wir brauchen also noch eine Lookup-Tabelle für die Paccie-Bewegungen und eine für die Kraken.
In dieser Lookup-Tabelle bekommt jede Bewegungsmöglichkeit den Wert einer 2er-Potenz, nach oben frei wäre also der Wert 20=1, nach rechts wäre der Wert 21=2, nach unten 22=4 und nach links 23=8. Bei mehr als einer freien Richtung werden die entsprechenden Werte addiert. So erhalten z.B. waagerechte Durchgänge den Wert für rechts+links=10, senkrechte oben+unten=5, Kreuzungen den Wert 15.
zum Seitenanfang | ||
zum Seitenende |
... na, möglichst schnell und kompakt! :-)
Die Levels des Spiels sind abgelegt in einem Feld der Dimension xmax*ymax, dabei bedeutet
Zweckmäßigerweise wird aber nicht das ganze Feld als solches generiert und gespeichert, sondern es wird in einen String umgewandelt, die Nummer im Feld ist der Platz des Buchstaben in einem Code, hier:
0123456789ABCDEF
. Dies nimmt weniger Platz ein, auch sieht der Code übersichtlicher aus. Gleiches gilt für die Lookup-Tabellen.
Bei der Übergabe sind mehrere Varianten denkbar. Allen gemein ist aber, dass eine Datei levels.js
angelegt wird, in denen in Javascript-Notation die Felddimensionen, eventuell die Levelnamen, auf jeden Fall einen Hinweis der maximalen Zahl der bereits erstellten Levels und die Levels selbst abgelegt sein sollten. Da das einzelne Level aus Codeersparnis in einem String abgelegt ist, gehört da auch ein Teil rein, der aus dem String das Level generiert. Diese levels.js
wird von Leveleditor und Hauptprogramm benutzt.
Der Level-Teil enthält also die Strings t
für das Spielfeld f
und f0
, pm
für die Bewegungsrichtungen des Paccies fp
und km
für die Bewegungsrichtungen der Kraken fk
, zusätzlich wird eine Liste der Höhlen h_liste
erstellt. Von Höhle n soll es nach Höhle n+1 gehen, danach wieder nach 0. Am zweckmäßigsten ist eine Liste, die bei 0 beginnt, da modulo-geeignet. Und weil ich keine Lust auf endloses Poppen zu Beginn eines neuen Levels habe, wird die gerade gültige Maximalgröße über h_count
geregelt.
levels.js
, I. Definitionen// Levels fuer Paccie //------------------------------------------------------------ maxlevels=24; // ----------------------------------------------------------- xmax=16; ymax=12; dx=24; dy=24; // Felddimensionen, Blockgröße code="0123456789ABCDEF"; // Pack-Code für die Felder var anz_chips,start; // zu fressende Chips, Paccie-Start var f=new Array(); // aktuelles Spielfeld var f0=new Array(); // Spielfeld-Backup var fp=new Array(); // Lookup-Tabelle Paccie-Move var fk=new Array(); // Lookup-Tabelle Krakenmove var h_count; // Anzahl Höhlen var hoehlen=new Array(); // Höhlen als Ring var nester=new Array(); // Krakennester/Start function lade_level(nr,flag) { var i,t,pm,km; if (nr==1) { t="1111111111111111132223222232223112111211212111211216221221226121 12111242112111211252221214222221122222121422222112111242112111211216221 221226121121112112121112113222322223222311111111111111111"; pm="044444444444444026AAAEAAEAEAAAC8259575975B5D535825A2ADA69A7A8A5825C 575A5925D565827EEEDA5827EEED827BBBDA5827BBBD8259575A5C25D535825A2ADA3CA 7A8A5825C575C75E5D565823AAABAABABAAA980111111111111110"; km="044444444444444026AAAEAAEAEAAAC8259175975B5D135825822DE69A788258258 477AD965D465825E6EDB5A6FEEED827ABBDE5A3FBBBD8259177ADC35D135825822DB3CA 78825825C475C75E5D465823AAABAABABAAA980111111111111110"; } else if (nr==2) { t="1111111111111111132222222222223112111111111111211322222222222231 11111112211111111422222222222241142222252222224111111112211111111322222 222222231121111111111112113222222222222311111111111111111"; pm="044444444444444026AAAAAAAAAAAAC825D555555555575823AAAAAEEAAAAA98015 55577DD555510026EEEEFFEEEEC80023BBBBFFBBBB98004555577DD55554026AAAAABBA AAAAC825D555555555575823AAAAAAAAAAAA980111111111111110"; km="044444444444444026AAAAAAAAAAAAC825D555555555575823AAAAAEEAAAAA98055 55577DD55555026EEEEEBFEEEEEC823BBBB9F7BBBBB9805555576DD55555026AAAAABBA AAAAC825D555555555575823AAAAAAAAAAAA980111111111111110"; } else if (nr==3) { ...
Vor der Darstellung muss der String wieder in das numerische Feld umgewandelt werden, code.indexOf(t.charAt(i))
ist dabei das Gegenstück zu den im Level-Editor selbst verwendetem t=t+code.charAt(f[i])
. Da der Level-Editor nur das Feld f
braucht, wird ein flag
mit übergeben, ob ein Spielfeld aus den restlichen Daten aufgebaut wird oder nicht:
levels.js
, II. Erzeugen der Felderh_count=0; // Höhlen initialisieren nester[0]=0; // Krakennester initialisieren anz_chips=0; // zu fressende Punkte bestimmen start=-1; // Paccie-Startfeld suchen for (i=0; i<t.length; i++) // Stringlänge absuchen { f0[i]=code.indexOf(t.charAt(i)); // Belegung berechnen if (flag) // Level aufbereiten: Bewegungen, Startpositionen { f[i]=f0[i]; // Backup für Levelwiederholung fp[i]=code.indexOf(pm.charAt(i)); // mögliche Bewegungen Paccie fk[i]=code.indexOf(km.charAt(i)); // mögliche Bewegungen Kraken if (f0[i]==2) anz_chips++; // Pillen hochzählen else if (f0[i]==4) // Krakennester { nester[0]++; nester[nester[0]]=i; } else if (f0[i]==5) { start=i; f[i]=0; } // Start Paccie else if (f0[i]==6) // Höhlenliste { h_liste[h_count]=i; h_count++; } } } } // Funktionsende
Bliebe noch zu klären, wie der Levelcode in die Datei kommt. - Hier ist Handarbeit angesagt: der Levelcode wird in ein Textfeld geschrieben und markiert. So weit, so gut! Nun muss man händisch den markierten Text in die Zwischenablage speichern und in den levels.js
, der in einem Texteditor geöffnet ist, an der entsprechenden Stelle einfügen.
Sorry, aber Javascript kann nicht in Dateien schreiben, also deshalb der Umweg über die Zwischenablage!
levels.js
liefert an das Hauptprogramm:
maxlevels
xmax
, ymax
dx
, dy
f
, f0
) mit Startlöchern der Kraken nester
und des Paccies start
fp
und der Kraken fk
hcount
und Positionen der Höhlen h_liste
anz_chips
zum Seitenanfang | ||
zum Seitenende |
Viel Spielraum hat man beim Layout nicht. Ganz oben die Überschrift, links die Spielfeldtabelle, die Elementauswahl direkt darunter, das Textfeld, in dem der erstellte Code gezeigt wird, schließt sich an. Rechts daneben die Buttons für die Steuerung "Zurücksetzen", "Code zeigen" u.ä., darunter die Buttons zum Laden der Levels:
Kopfzeile | |
Spielfeld mit verweissensitiven Grafiken
"Vorratsliste" mit Marker
Textarea für den Javascript-Code
|
Reset- und Anzeige-Buttons
Level-Lade-Buttonfeld
|
Mit deser Aufteilung kann ich am schnellsten arbeiten. Häufig benutzte Elemente sind direkt am Spielfeld, die Maus hat kurze Wege. Auch beim Levelladen bevorzuge ich die etwas übersichtlichere Variante mit verschiedenen Buttons gegenüber einer Auswahlliste, laden ist zum einen 1 Klick weniger, zum anderen kann ich die Buttons besser treffen als eine Option in einer Auswahlliste.
Im Header fasse ich mich extrem kurz, die Seite ist nicht für die Suchmaschinen gedacht.
Spielfeld und Vorratsliste in der linken Tabellenzelle werden mit Javascript erzeugt, da deren Dimensionen von den Definitionen in level.js
abhängen, das Textfeld schließt sich unten an. Auch die Lade-Buttons in der rechten Tabellenzelle werden mit Javascript erzeugt.
<!doctype html public "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <head><title>Level - Editor</title> <META HTTP-EQUIV="content-type" CONTENT="text/html; charset=windows-1252"> <link rel="stylesheet" type="text/css" href="../gamecraft.css"> <script type="text/javascript" src="levels.js"> </head> <body onload="leeren()"> <h4>Pacfish-Editor by uja </h4> <table> <tr> <td> <script type="text/javascript"> // die Spielfeldgröße und Anzahl möglicher Inhalte sind in // levels.js initialisiert, also wird dieser Teil mit Javascript // erzeugt: document.writeln(spielfeld()); document.writeln('<hr><br>'+muster()); </script> <-- Textarea für die zu erzeugenden Daten in einem Formular --> <form name="form1"> Feldcode:<br> <textarea name="my_code" cols=96 rows=8>t='';</textarea> <br clear=all> </form> </td> <td> <div class="button"><a href="javascript:leeren()">leeren</a></div> <div class="button"><a href="javascript:zeigen()">Code zeigen</a></div> <hr> <script type="text/javascript"> // die Levelzahl ist in levels.js initialisiert, // also wird dieser Teil auch mit Javascript erzeugt: t=''; for (var i=0; i<maxlevels; i++) { t=t+'<div class="button">'; t=t+'<a href="javascript:laden('+i+')">Level '+i+'</a>'; t=t+'</div>'; } document.writeln(t); </script> </td> </tr> </table> </body> </html>
Noch ein Wort zum Textfeld: es ist wichtig, dass beim Erzeugen schon irgend etwas hineingeschrieben wird, sonst wird es von einigen Browsern (IE4?) unterschlagen. Später soll da genau der Text erscheinen, der in den Leveleditor zu kopieren ist. Man sollte das Feld also eher großzügig dimensionieren.
Fehlen noch die Funktionen spielfeld()
und muster()
, ersteres sollte klar sein, letzteres sind 2 Bilderreihen untereinander, in der ersten Reihe die zu setzenden Elemente, in der zweiten "Leerbilder" und ein Pfeil für das gerade markierte Element.
... <td> <a href="javascript:setze(xx)"> <img src="leer.gif" width=24 height=24 border=0 alt="" name="ixx"> </a> </td> ...
Dieser Code liefert den einzuschiebenden Text, document.writeln(spielfeld())
schreibt ihn hin, viola!©uja
spielfeld
function spielfeld() { var i,j,k=0, var t='<table border=0 cellpadding=0 cellspacing=0 class="spielfeld">'; t=t+'<tr>'; for (j=0; j<ymax; j++) { t=t+'<tr>'; for (i=0; i<xmax; i++) { t=t+'<td><a href="javascript:setze('+k+')">'; t=t+'<img src="leer.gif" width='+dx+' height='+dy+' border=0 '; t=t+'alt="" name="i'+k+'"></a></td>'; k++; } t=t+'</tr>'; } t=t+'</table>'; return t; }
Ähnlich sieht es mit der Auswahlleiste aus, die Bilder bekommen aber nicht den Namen ixx, sondern ipxx. Auch bekommen nur die Bilder in der untersten Reihe Namen, da nur diese zu ändern sind. reihe[xx]
enthält die Bildnamen, s. nächstes Kapitel, preload.
... <td> <a href="javascript:waehle(xx)"> <img src="wattauchimmer.gif" width=24 height=24 border=0 alt=""> </a> </td> ... <td> <a href="javascript:waehle(xx)"> <img src="leer_oder_pfeil.gif" width=24 height=12 border=0 alt="" name="iwxx"> </a> </td> ...
muster
function muster() { var i,t='<table border=0 cellpadding=0 cellspacing=0><tr>'; for (i=0; i<reihe.length; i++) { t=t+'<td><a href="javascript:waehle('+i+')">'; t=t+'<img src="'+reihe[i]+'.gif" width='+dx+' height='+dy+' border=0 '; t=t+'hspace=2 alt=""></a><br clear=all>'; t=t+'<img src="leer.gif" width='+dx+' height=16 border=0 '; t=t+'hspace=2 alt="" name="ip_'+i+'"></a></td>'; } t=t+'</tr></table>'; return t; }
zum Seitenanfang | ||
zum Seitenende |
Was macht so ein Level-Editor?
Bevor wir uns aber an diese Liste machen, muss erst mal einiges an Vorarbeiten erledigt werden. Vorladen der Bilder können wir uns diesmal sparen, da der Level-Editor nur lokal verwendet werden kann. Aber die Reihe(nfolge) der Bilddateien muss erstellt werden, um die Auswahlleiste zügig programmieren zu können:
<script type="text/javascript"> zmax=xmax*ymax; // Feldgröße var gewaehlt=0; // Leerfeld vorselektiert var reihe=new Array('leer','mauer','p_1','p_2','krake_r','rechts_1','hoehle'); // ----------------------------------------------------------------------------
Bequem ist eine Funktion zum Anzeigen des aktuellen Feldinhaltes nr
mit dem Bild reihe[f[nr]]
:
function zeige_feld(nr) { document.images['i'+nr].src=reihe[f0[nr]]+'.gif'; }
Beim Initialisieren wird das zu bearbeitende Feld und die Auswahlliste auf einen definierten Zustand gebracht, zweckmäßig ist hier: eine Aussenmauer, und alle inneren Werte :-) auf einfachen Punkt setzen:
function leeren() { var i; for (i=xmax; i<zmax-xmax; i++) f[i]=2; // Futter for (i=0; i<xmax; i++) // obere und untere Mauer { f[i]=1; f[zmax-i-1]=1; } for (i=1; i<(ymax-1); i++) // rechte und linke Mauer { f[i*xmax]=1; f[i*xmax+xmax-1]=1; } for (i=0; i<zmax; i++) zeige_feld(i); waehle(0); // Leerfeld auswählen }
Die bereits in levels.js
einprogrammierten Levels werden wie folgt eingelesen. Da sie in einem String vorliegen, müssen sie vor dem Verwenden bearbeitet werden. Die Funktion dazu ist gespeichert in levels.js
, da auch das Hauptprogramm darauf zugreifen muss. Sie wurde bereits in Kap.2.2. erklärt.
function laden(nr) { lade_level(nr,false); // wir brauchen nur das Feld f for (var i=0; i<zmax; i++) zeige_feld(i); // ... und anzeigen }
Sollen neu erstellte Levels geladen werden, so ist in levels.js maxlevels
anzupassen und der Editor neu zu laden.
Die Aufgabe "Feld bearbeiten" erledigen 2 Funktionen, die erste wählt ein Element aus, die zweite setzt es ins Feld. gewaehlt
erhält den Wert des gewählten Elementes. Damit dieses auch richtig angezeigt wird, kann man mehrere Wege gehen:
leer
und die des gewählten Bildes auf marke
leer
, setzt danach die neue marke
gewaehlt
vorher mit einem gültigen Wert initialisiert wurde.
function waehle(nr) { document.images['ip_'+gewaehlt].src="leer.gif"; gewaehlt=nr; document.images['ip_'+nr].src="marke.gif"; }
Setzen des gewählten Elementes ist noch simpler, das angeklickte Feld bekommt den Wert gewaehlt
, anzeigen lassen, das war's!
function setze(nr) { f[nr]=gewaehlt; zeige_feld(nr); }
Javascript kann nicht in Dateien schreiben (abgesehen mal von Cookies), also ist "abspeichern" hier mit etwas Handarbeit verbunden. Mal sehen, wie weit uns Javascript und HTML entgegenkommen.
Aus den Anforderungen sehen wir, dass ein String erstellt werden soll, aus dem der Feldinhalt wiedergewonnen werden kann. Noch bequemer ist es, wenn gleich der ganze einzufügende Text generiert wird, und diesen Weg bin ich auch gegangen. Der Text wird im Textfeld angezeigt, dieses komplett markiert und fokussiert, so dass ich sofort mit <strg-Einf> das ganze Gesocks in die Zwischenablage speichern kann und mit <shift-Einf> in den Quellcode von levels.js
zwischen die if
-Abfragen einfügen kann.
levels.js
... else if (nr==wattauchimmer) { // hier kommt das ganze Zeuch rein! } ...
Was passiert nun bei Klick auf "Anzeigen"? Erst mal werden die Bewegungsmöglichkeiten für Paccie und die Kraken berechnet und in den Feldern fp
und fk
abgespeichert. Danach werden alle Felder in Strings umcodiert. Diese "Primärstrings" werden in einen weiteren String eingebettet, der anschließend den einzufügenden Code enthält. Dieser Komplett-String wird im Textfeld angezeigt und markiert. Zuletzt wird der Fokus auf dieses Textfeld gesetzt. - Mehr kann man dem Anwender hier nicht entgegenkommen!
Die Funktion level_aufbereiten()
berechnet die Bewegungsmöglichkeiten der Figuren für alle Positionen. Hierfür müssen die Nachbarfelder abgecheckt werden. Im Mittelteil sind dies einfach position +-1
für rechts und links und position +-xmax
für unten und oben. Es wurde ein "wrap-around" oder Torusfeld gewählt, d.h. bei Überschreiten der Grenzwerte 0 und xmax-1 b.z.w. ymax-1 werden die entsprechenden Koordinaten an der anderen Seite genommen.
Diese Liste soll es noch mal verdeutlichen:
Die Funktion ist_frei(nr,figur)
bestimmt, ob die Figur auf das Feld nr
darf. Die entsprechenden 2er-Potenzen für o, r, u oder l werden addiert, viola!
function level_aufbereiten() { var i,o,u,r,l,x,y,frei,k=0; for (i=0; i<zmax; i++) { x=i%xmax; y=Math.floor(i/xmax); // Linearkoordinaten nach 2-dim o=(y+ymax-1)%ymax; o=o*xmax+x; // Torus-Nachbar oben r=(x+1)%xmax; r=r+xmax*y; // Torus-Nachbar rechts u=(y+1)%ymax; u=u*xmax+x; // Torus-Nachbar unten l=(x+xmax-1)%xmax; l=l+xmax*y; // Torus-Nachbar links frei=0; if (ist_frei(o,'krake')) frei=frei+1; if (ist_frei(r,'krake')) frei=frei+2; if (ist_frei(u,'krake')) frei=frei+4; if (ist_frei(l,'krake')) frei=frei+8; fk[i]=frei; frei=0; if (ist_frei(o,'paccie')) frei=frei+1; if (ist_frei(r,'paccie')) frei=frei+2; if (ist_frei(u,'paccie')) frei=frei+4; if (ist_frei(l,'paccie')) frei=frei+8; fp[i]=frei; } }
ist_frei(posi,figur)
function ist_frei(posi,figur) { var ok=((f0[posi]==0) || (f0[posi]==2)); // frei oder Punkt if (!ok) ok=(f0[posi]==3); // oder Kraftpille if (!ok) { if (figur=='krake') ok=(f0[posi]==4); // nur für Kraken passierbar // nur für Paccie passierbar, hier wurde auch das Goodie-Feld aufgenommen if (figur=='paccie') ok=((f0[posi]==5) || (f0[posi]==6)); } return ok; }
Die nächste Aufgabe ist das Erzeugen der Strings aus den Feldern f
, fp
und fk
.
Hierbei wird aus dem in levels.js
angegebenem code
der f[i].te Buchstabe genommen, wobei bei 0 angefangen wird, zu zählen: t=t+code.charAt(f[i]);
. Der String t="
wird davorgehängt, der String ";
schließt die zu erzeugende Javascript-Zeile ab, die danach etwa folgedes Aussehen haben sollte:
t="1111111111111111132223222232223 ...... 2311111111111111111";
Eine Feinheit noch: danach füge ich einen Zeilenumbruch ein: \n
, bevor das nächste Feld auf die gleiche Art dahintergehängt wird:
zeigen()
function zeigen() { var i,t='t="'; level_aufbereiten(); for (i=0; i<zmax; i++) t=t+code.charAt(f[i]); t=t+'";\n'; t=t+'pm="'; for (i=0; i<zmax; i++) t=t+code.charAt(fp[i]); t=t+'";\n'; t=t+'km="'; for (i=0; i<zmax; i++) t=t+code.charAt(fk[i]); t=t+'";'; document.form1.my_code.value=t; document.form1.my_code.focus(); document.form1.my_code.select(); }
document.form1.my_code.value=t
stellt den Text im Textfeld my_code
des Formulars form1
dar, document.form1.my_code.focus()
setzt den Fokus ins Feld, document.form1.my_code.select()
wählt ihn an: fertig zum Kopieren die Zwischenablage.
zum Seitenanfang | ||
zum Seitenende |
Damit der Spielfluss nicht unterbrochen wird, sollten alle benötigten Bilder schon mal über die Internet-Leitung in den Computer gekrochen sein, sie müssen diesmal also vorgeladen werden. In Javascript erzeugt man schlicht und einfach Image-Objekte, denen man die gewünschten Bilddateien als src
-Eigenschaft zuweist. Sobald die Anweisung ausgeführt wird, werden die Dateien geladen und verbleiben im Cache. Das Ganze sieht folgendermaßen aus:
var bild=new Array('leer','mauer','p_1','p_2','nest','pfeil','hoehle', 'langsamer','schneller','l_extra','p_extra','segler', 'shooter','freeze'); var p_bild=new Array('oben_0','oben_1','rechts_0','rechts_1', 'unten_0','unten_1','links_0','links_1','tot'); var k_bild=new Array('krake_0','krake_1','krake_2','krake_3', 'krake_k','krake_t','krake_r'); var pf_bild=new Array('oben','rechts','unten','links'); var ima=new Array(); var ima_p=new Array(); var ima_k=new Array(); var ima_pf=new Array(); for (i=0; i<bild.length; i++) { ima[i]=new Image(); ima[i].src=bild[i]+'.gif'; } for (i=0; i<p_bild.length; i++) { ima_p[i]=new Image(); ima_p[i].src=p_bild[i]+'.gif'; } for (i=0; i<k_bild.length; i++) { ima_k[i]=new Image(); ima_k[i].src=k_bild[i]+'.gif'; } for (i=0; i<pf_bild.length; i++) { ima_pf[i]=new Image(); ima_pf[i].src=pf_bild[i]+'.gif'; }
Das Layout wurde diesmal nicht mit Tabellen erstellt, sondern mit Divs/Layer. Es gibt einen Layer für die Überschrift, einen für's Spielfeld, einen für die Anzeigen und einen für Punktestand/Buttons. Diese erhielten je die Schichtebene 0 oder 1, da sie zum Hintergrund rechnen. Auch die 5 Sprites bestehen aus Layers, aber mit höheren, untereinander verschiedenen Schichtebenen, um den Browser bei Kollisionen nicht in Probleme zu bringen (vgl. Kap. 4.6.)
Entgegen der Beschreibung in einigen Büchern über DTHML lassen sich im NN4 Schichten, die mit DIV
erzeugt wurden, nicht bewegen. Und der Rest der Welt kennt keine Layers. Das heisst, es müssen eigentlich 2 Dokumente geschrieben werden, einmal für NN4 und einmal für alle anderen Browser. NN4 sträubt sich, wenn einmal eine Seite aufgebaut wird, irgendwo was zu verändern, wobei alle folgenden Elemente verschoben werden müssten. Bei solchen Sachen besteht er auf einem Reload, was dem Spielfluss aber extrem abträglich ist.
Dies ist der Haupt-Nachteil von DHTML. Javascript gab es erst mal nur für den Netscape. Dann wurden die Entwickler vom IE wach, als sie sahen, was für feine, kleine Sachen man damit machen kann, entwickelten etwas Ähnliches (das Gleiche ging wahrscheinlich aus lizenzrechtlichen Gründen nicht) und nannten es Jscript (situs vijava script isset abernet). Natürlich war Jscript viel mächtiger und ausserdem in Windows integriert, was zunächst etliche Vorteile bescherte, aber im Laufe der Zeit zusammen mit Active-X-Elementen zu gefährlichen Sicherheitslücken im Browser führte.
Und weil zu diesem frühen Zeitpunkt DHTML zwar schon ein Schlagwort, aber noch keine der Funktionen und Objekte durch irgendein Konsortium geregelt waren, gab es zwei leicht verschiedene DOMs (Document Object Models)
Wegen dieser Problematik wurde in diesem Spiel fast der gesamte body
-Teil in Javascript geschrieben. Das Strickmuster soll am Layer spielfeld
für das Spielfeld gezeigt werden:
var t,t1=spielfeld(); // t1 = Layerinhalt if (my_browser=='nn4') { t='<layer name="feld" visibility="show" '; t=t+'top="40" left="24" z-index="1" '; t=t+'width="388">'+t1+'</layer>'; } else { t='<div id="feld" style="position:absolute; visibility:visible; '; t=t+'top:40px; left:24px; width:388; z-index:1" '; t=t+'width:388px;>'+t1+'</div>'; } document.writeln(t);
Die Funktion spielfeld()
liefert den HTML-Teil zum Erzeugen des Spielfeldes, dieses wird je nach Browser in layer
s oder div
s verpackt, dann hingeschrieben. Dieses Layer/Div erscheint sichtbar an den Browserkoordinaten y=40/x=24/z=1 und haben eine Breite von 388 Pixeln.
Als Vorarbeit müssen wir erst mal bestimmen, mit welchem Browser (DOM) gearbeitet wird. Wir unterscheiden hier zwischen nn4, nn5, ie4, ie5 und sonstige:
var xyz=new Array(3); // temp.Wert fuer Koordinaten der Objekte var my_browser=get_browser(); Kennung des Browsers fuer DOM function get_browser() { var t=navigator.appName; if (t=='Netscape') t='nn'; else if (t.search(/Internet.+/)>-1) t='ie'; if (navigator.appVersion.substring(0,1) == "4") t=t+4; else if (navigator.appVersion.substring(0,1) == "5") t=t+5; if (t=='ie4') if (document.documentElement) t='ie5'; return t; }
Warum fragen wir nicht einfach auf Objekte ab? - Es gibt Browser wie z.B. Opera, die meinen, ein Objekt zu kennen,
aber dann kennen sie nicht mehr als den Namen dieses Objektes. Sagt ihm der Anwender aber, er soll das DOM von IE
oder Mozilla/Netscape benutzen, dann klappt die Zusammenarbeit auf einmal.
Desweiteren sollte man die "crossbrowser"-Funktionen immer so anlegen, dass im letzen Fall (sonstige)
das neueste DOM (nn5) herangezogen wird.
Somit werden erst die "bewährten" Modelle durchprobiert, bevor es an etwas noch nicht ganz Ausgegorenes geht.
Da der Zugriff auf die Elemente dieses Layers, in diesem Spiel meist Bilder, von Browser zu Browser unterschiedlich ist, ist das wieder ein Fall für die Crossbrowser-Bibliothek. Die Unterschiede sollen am Bild my_ima='ima1'
im Layer my_lay='lay1'
gezeigt werden, dessen Inhalt verändert werden soll:
Browser | Ersetzen eines Bildes im Layer |
---|---|
NN4 | document.lay1.document.ima1.src=bildname document.layers[my_layer].document.images[my_bild].src=bildname |
IE4 | document.all.ima1.src=bildname document.all[my_bild].src=bildname |
NN5 | document.images[my_bild].src=bildname |
W3C | document.getElementById(my_bild).setAttribute('src',bildname); |
In den ersten Beispielen wird einer Objekt-Eigenschaft direkt ein anderer Wert untergeschoben (Schwarzarbeiter), im letzten wird eine Funktion aufgerufen, die veranlasst, dass der Eigenschaft src
der neue Wert zugeschoben wird (Bürohengst). Welche Funktionsgruppe schneller reagiert, darf geraten werden. Aber nichtsdestotrotz sind alle schnell genug, die Probleme gibt es woanders.
Die obige Tabelle wird umgesetzt zu:
// Aufruf: set_bild(Layername,Bildname,Bildcode) function set_bild(lay,bild,datei) { if (my_browser=='ie4') document.images[bild].src=datei; else if (my_browser=='nn4') document.layers[lay].document.images[bild].src=datei; else if (my_browser=='nn5') document.images[bild].src=datei; // Mozilla >0.9 else document.getElementById(bild).setAttribute('src',datei); // WC3 }
zum Seitenanfang | ||
zum Seitenende |
Da Layers beim "normalen" Aufbau einer HTML-Seite extra behandelt werden, müssen alle anderen Teile auch in Layers verpackt werden, da sie sonst von den Koordinaten 0/0 aus dargestellt werden, deren Platz aber nun schon durch das Spielfeld eingenommen wurde. Sie würden sonst hinter dem Spielfeld auf Ebene 0 erscheinen.
Bei Teilen, die einfach dargestellt und danach nicht mehr geändert werden sollen wie die Kopfzeile, gibt es nichts weiter zu beachten. Bei Teilen, die bei Anklicken eine andere Seite oder Javascript-Funktion aufrufen sollen, ist nur darauf zu achten, dass der verweissensitive Teil an oberster Stelle liegt. Also "Business as usual". Damit sind Überschrift, Buttons für Neu, Hilfe, Bestenliste, Rücksprung erschlagen, die, abgesehen vom Layer, in dem sie stecken, auf die gleiche Weise angelegt werden wie die Buttons im Level-Editor.
Ws bleibt, sind Elemente für die Anzeigen: wir brauchen eine Anzeige der Paccie-Leben, eine, die anzeigt, ob eine Harpune ergattert wurde, und eine Anzeige des Pfeilvorrates.
Man könnte wieder ein Formular-Input-Element dafür nehmen, aber es sieht netter aus, wenn die Paccies und Pfeile unterhalb des Spielfeldes als Bildelemente eingefügt werden. Dazu erzeuen wir uns das passende Layer und füllen es mit der entsprechenden Anzahl Leerbilder. Der Bilderaustausch wird ausgeführt von der Bibliotheksfunktion set_bild(layer,bild,bilddaten)
aus Kap. 3.1..
// Layers erzeugen: if (my_browser=='nn4') { t='<layer name="anzeige" '; t=t+'top="336" left="24" width="388" z-index="0">'; } else { t='<div id="anzeige" style="position:absolute; '; t=t+'top:336px; left:24px; width:388; z-index:0">'; } // Anzeige Leben: for (i=0; i<pmax; i++) { t=t+'<img src="rechts_0.gif" alt="Leben" width=24 height=24 '; t=t+'name="p_'+i+'">'; } t=t+'<br clear=all>'; // Anzeige Harpune: t=t+'<img src="shooter.gif" alt="Shooter" '; t=t+'width=24 height=24 name="p_sh">'; t=t+'<br clear=all>'; // Pfeilvorrat: for (i=0; i<pfmax; i++) { t=t+'<img src="pfeil.gif" alt="Pfeile" '; t=t+'width=24 height=24 name="pf_'+i+'">'; } // Layers abschließen: if (my_browser=='nn4') t=t+'</layer>'; else t=t+'</div>'; // ... und ab in den HTML-Quältext: document.writeln(t);
Ein paar Anzeigen müssen aber doch mit Formular-Input-Elementen realisiert werden: Punktestand und Level. Platz- und designmäßig passen diese am besten zu den Buttons, also wurden sie in das gleiche Layer gesteckt. Zum Ansprechen dieser Elemente muss allerdings wieder die Crossbrowser-Bibliothek herangezogen werden:
// Aufruf: set_input(Layername,Formularname,Inputname,neuer_Text) function set_input(lay,formn,inam,wert) { if (my_browser=='nn4') document.layers[lay].document.forms[formn].elements[inam].value=wert; else if (my_browser=='ie4') document.forms[formn].elements[inam].value=wert; else if (my_browser=='nn5') document.forms[formn].elements[inam].value=wert; else document.getElementById(inam).setAttribute('value',wert); //W3C }
zum Seitenanfang | ||
zum Seitenende |
Die Crossbrowser-Routinen sind in einer allgemein zugängliche Datei, es werden nur die in diesem Spiel verwendeten Routinen vorgestellt. Bisher haben wir folgende Teile:
Damit sieht der Quelltext bisher folgendermaßen aus:
<!doctype html public "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <head><title>ujas Pacfish 1.01</title> <meta name="author" content="UJa, Ulrike Jahnke-Soltau"> <META HTTP-EQUIV="content-type" CONTENT="text/html; charset=windows-1252"> <meta name="keywords" content="PacFisch, PacFish, Pacman, Barsch, Labyrinth, Javascript-Spiel, javascript game online"> <meta name="description" content="Ein Bärschlein in einem See hat einen gewaltigen Appetit und frisst alles, was vor sein Mäulchen kommt. - Pacman-Variante"> <link rel="stylesheet" type="text/css" href="../gamecraft.css"> <script type="text/javascript" src="../_crossbr.js"></script> <script type="text/javascript" src="levels.js"></script> <script type="text/javascript"> // ein paar nützliche Konstanten: zmax=xmax*ymax; // Anzahl Felder ofx=24; ofy=40; // Spielfeldoffsets pmax=5; // maximale Paccies pfmax=xmax-pmax-2; // maximale Wurfgeschosse n_speed=4,h_speed=8,l_speed=2; // Geschwindigkeiten // ein paar nützliche Variable: var i,level=0,punkte=0; // selbsterklärend var pakt=3,pfakt=0,shooter=false; // Leben, Pfeile, Harpune var goodie=0; // derzeitiges Bonbon var ri=1,oldri=1,schussri=-1; // Richtung neu, alt, Schuss var tic,aktiv=false,gestoppt=true; // Steuerung für Timer-Routinen // ----------------------------------------------------------- function spielfeld() { var i,j,k=0,t='<table border=0 cellpadding=0 cellspacing=0>'; for (j=0; j<ymax; j++) { t=t+'<tr>'; for (i=0; i<xmax; i++) { t=t+'<td><img src="leer.gif" width='+dx+' height='+dy+' '; t=t+'border=0 alt="" name="i_'+k+'"></td>'; k++; } t=t+'</tr>'; } t=t+'</table>'; return t; } // ein paar Info- und Hilfsfenster: function hilfe() { var win2=window.open('hilfe.htm','popup', 'width=592,height=376,scrollbars=yes'); } function hiscore() { var win2=window.open('hiscore.htm','popup', 'width=592,height=376,scrollbars=yes'); } // so könnte ein Highscore-Eintrag aufgerufen werden // mit einem rudimentären Schutz von Pfuschern, // Weitere gehören in das auswertende Script: function eintrag(p) { var win2=window.open('eingabe.php?punkte='+p+'&kenn='+kenn,'popup', 'width=592,height=376,scrollbars=yes'); } // Bilder vorladen: ----- var bild=new Array('leer','mauer','p_1','p_2','nest','pfeil','hoehle', 'langsamer','schneller','nspeed','l_extra','p_extra','segler', 'shooter','freeze'); var p_bild=new Array('oben_0','oben_1','rechts_0','rechts_1', 'unten_0','unten_1','links_0','links_1','tot'); var k_bild=new Array('krake_0','krake_1','krake_2','krake_3', 'krake_k','krake_t','krake_r'); var pf_bild=new Array('oben','rechts','unten','links'); var ima=new Array(); var ima_p=new Array(); var ima_k=new Array(); var ima_pf=new Array(); for (i=0; i<bild.length; i++) { ima[i]=new Image(); ima[i].src=bild[i]+'.gif'; } for (i=0; i<p_bild.length; i++) { ima_p[i]=new Image(); ima_p[i].src=p_bild[i]+'.gif'; } for (i=0; i<k_bild.length; i++) { ima_k[i]=new Image(); ima_k[i].src=k_bild[i]+'.gif'; } for (i=0; i<pf_bild.length; i++) { ima_pf[i]=new Image(); ima_pf[i].src=pf_bild[i]+'.gif'; } </script> </head> <body> <div id="kopf" style="position:absolute; top:8px; left:24px; width:388; text-align:center; z-index:0"> <h4><img src="rechts_0.gif" width=24 height=24 alt="Paccie" hspace=12> PacFish V.1.01 <small> by uja </small> <img src="links_0.gif" width=24 height=24 alt="Paccie" hspace=12> </h4> </div> <script type="text/javascript"> // Spielfeld: var i,t,t1=spielfeld(); // t1 = Layerinhalt if (my_browser=='nn4') { t='<layer name="feld" visibility="show" '; t=t+'top="40" left="24" z-index="1" '; t=t+'width="388">'+t1+'</layer>'; } else { t='<div id="feld" style="position:absolute; visibility:visible; '; t=t+'top:40px; left:24px; width:388; z-index:1" '; t=t+'width:388px;>'+t1+'</div>'; } document.writeln(t); // Buttons und Anzeige Level, Punkte: if (my_browser=='nn4') { t='<layer name="knoeppe" visibility="show" '; t=t+'z-index="0" top="48" left="432" width="160">'; } else { t='<div id="knoeppe" style="position:absolute; '; t=t+'top:48px; left:432px; width:160; z-index:0">'; } t=t+'<form name="form1" action="nix" onsubmit="return false;">'; t=t+'<table><tr><td>Level:</td><td><input type="text" '; t=t+'name="level" size=8></td></tr>'; t=t+'<tr><td>Punkte:</td>'; t=t+'<td><input type="text" name="punkte" size=8></td></tr>'; t=t+'<tr><td colspan=2>'; t=t+'<div class="button"><a href="javascript:neu(true)">neu</a></div>'; t=t+'<div class="button"><a href="javascript:hilfe()">Hilfe</a></div>'; t=t+'<div class="button"><a href="javascript:hiscore()">Bestenliste</a></div>'; t=t+'<div class="button"><a href="javascript:window.close()"wech</a></div>';'; t=t+'<div class="button"><a href="http://www.gamecraft.de/" target="_blank"> t=t+'Menu nachladen</a></div>'; t=t+'</td></tr>'; t=t+'</table></form>'; if (my_browser=='nn4') t=t+'</layer>'; else t=t+'</div>'; document.writeln(t); // Pfeile, Harpune, Leben: if (my_browser=='nn4') { t='<layer name="anzeige" visibility="show" '; t=t+'z-index="0" top="336" left="24" width="388">'; } else { t='<div id="anzeige" style="position:absolute; '; t=t+'top:336px; left:24px; width:388; z-index:0">'; } for (i=0; i<pmax; i++) t=t+'<img src="rechts_0.gif" alt="Paccie" width=24 height=24 name="p_'+i+'">'; t=t+'<br clear=all>'; t=t+'<img src="shooter.gif" alt="Shooter" width=24 height=24 name="p_sh">; t=t+'<br clear=all>'; for (i=0; i<pfmax; i++) t=t+'<img src="pfeil.gif" alt="Pfeile" width=24 height=24 name="pf_'+i+'">'; t=t+'<br clear=all>'; if (my_browser=='nn4') t=t+'</layer>'; else t=t+'</div>'; document.writeln(t); </script> </body> </html>
Warum erfolgt der Rücksprung des Dokumentes auf eine neue Seite? - Man findet seine Spiele an den unmöglichsten Orten wieder, beliebt waren in letzter Zeit "kastrierte" Fenster. Und wenn jemandem schon das eine Spiel gefallen hat, soll er auch von der korrekten Domain aus die anderen in voller Größe genießen können!
Da sich mittlerweile herumgesprochen hat, dass geklaute Seiten in Frames sich durch Framesprengen "befreien" können, gehen die Leute mittlerweile den Weg, die Seiten in einem Popup ohne Adresszeile und mit eingeschränkter Größe anzuzeigen. Also gibt es in jedem Spiel einen kleinen, feinen Button mit passender Beschriftung, der die korrekte Hauptseite in einem kompletten Fenster nachläd.
Noch ein Wort zur Bestenliste: Javascript ist eine Interpretersprache, die beiden großen Browser haben Javascript-Konsolen integriert, was bedeutet, dass man von da aus die Funktionen aufrufen kann. Damit ist Missbrauch Tür und Tor geöffnet. Es gibt *keine* Methode, um sich davor zu schützen, ausser: die Seite nicht zu veröffentlichen! (vgl. d.c.l.j., FFQ)
Da Javascript nicht in der Lage ist, externe Dateien zu lesen und zu schreiben, wird bei Eintrag ein Formular aufgerufen, das die Daten an ein Server-Script weitergibt. Auch hier gibt es Möglichkeiten der Manipulation. Man könnte als Abhilfe eine Kennnung mitschicken, die irgendwo generiert wird. Passt die nicht, streikt das Eingabe-Fenster. Diese Kennung kann aber auch geknackt werden, so dass das Script, was letztendlich die Daten einträgt, checken muss, ob die Daten keinen Schaden anrichten. Aus diesem Grund sind Spiele mit Highscoreliste die weitaus teuersten.
Wir haben jetzt das Aussehen der Seite festgelegt mit Spielfeld, Anzeigen, Buttons und den entsprechenden Funktionen in der Crossbrowser-Bibliothek für den Zugriff auf diese Elemente.
zum Seitenanfang | ||
zum Seitenende |
// Generieren von Sprites, Name=name, Layer=l_name: var pac=new sprite('pac',ima_p[2],0,0,8,24,24,0,2); var pfe=new sprite('pfe',ima[5],0,0,9,24,24,0,0); var kra=new Array(); for (i=0; i<4; i++) kra[i]=new sprite('kra'+i,ima_k[i],0,0,3+i,24,24,0,i); function sprite(name,bild,x,y,z,dx,dy,dz,zustand) { // --- Inhalt des Sprite-Layers: var t,t1='<img src="'+bild.src; t1=t1+'" width='+dx+' height='+dy+' alt="" name="i_'+name+'">'; // --- Sprite-Layer definieren if (my_browser=='nn4') { t='<layer name="l_'+name+'" visibility="hide" '; t=t+'z-index="'+z+'" top="'+y+'" left="'+x; t=t+''" width="'+dx+'" height="'+dy+'">'+t1+'</layer>'; } else { t='<div id="l_'+name+'" style="position:absolute; z-index:'+z+'; '; t=t+'visibility:hidden; top:'+y+'px; left:'+x+'px; '; t=t+'width:'+dx+'px; height:'+dy+'px; ">'+t1+'</div>'; } // --- das Sprite-Layer wird erstellt document.writeln(t); // --- und fix noch ein paar Eigenschaften einstellen: this.name=name; this.breite=dx; this.hoehe=dy; this.phase=0; this.zustand=zustand; this.x=0; this.y=0; this.z=z; this.ix=0; this.iy=0; this.dx=0; this.dy=0; this.dz=0; this.speed=n_speed; } // ---------------------------------------------------------
Die Sprites liegen erst mal bei den Koordinaten 0,0 in der Landschaft herum und bewegen sich nicht. Jedes ist in einer anderen Schicht, damit der Browser bei Kollision weiss, wie was darzustellen ist und nicht erst mal mit sich selbst zu tun hat, was zu Verzögerung der Bewegung führt. Damit das Gesamtbild nicht gestört wird, sind sie erst mal unsichtbar, bis sie mit plausiblen Koordinaten gefüttert werden. Dies geschieht zum erstenmal mit der Funktion neu(flag)
, die das entsprechende Level läd. Der Level-Editor übergibt die Startkoordinaten, setzt alles auf definierte Werte und lässt die Sprites erscheinen.
Die beiden Schalter, die die timergesteuerte Funktion regeln, werden auf Abmarsch gesetzt: gestoppt=false
wund aktiv=true
, zuletzt wird die timergesteuerte Funktion move_all()
angestoßen:
neu(flag)
function neu(flag) { gestoppt=true; // Timerroutine anhalten/auslaufen lassen kenn=rate_mal; // Kennung für Highscore if (flag) // von ganz vorne an { pakt=3; pfakt=0; level=0; punkte=0; shooter=false; } /// restliche Werte für das Level resetten: tic=0; goodie=0; schussri=-1; laden(level); zeige_level(level); zeige_punkte(punkte); zeige_pfeile(pfakt); zeige_shooter(shooter); // Paccie und Kraken erscheinen lassen: init_paccie(); for (var i=1; i<=nester[0]; i++) init_geist(i-1); // Timersteuerung, s.Kap.4.3. if (gestoppt) { aktiv=true; gestoppt=false; move_all(); } }
Sieht man sich die obige Funktion an, so fällt auf, dass nicht unmittelbar die Crossbrowser-Bibliothek für die Anzeigen angesprungen wird, sondern es geht über eine Zwischenstufe. Der Grund ist, dass dieser Quelltext zum einen 1:1 in eine andere Sprache übernommen werden kann, nur die untenstehenden Funktionen müssen angepasst werden. Zum anderen sind die Funktionsnamen, wie sie in neu(flag)
stehen, aussagekräftiger, was das weitere Entwickeln erleichtert. Die ausführenden Funktionen fasse ich in einem Block "Interfaces" zusammen:
// Laden und Anzeigen des Levels, // Lookup-Tabellen für Bewegungen holen: function laden(nr) { lade_level(nr,true); for (var i=0; i<zmax; i++) zeige_feld(i); } // Anzeige, ob Harpune in Besitz: function zeige_shooter(flag) { var k=ima[0]; if (flag) k=ima[13]; set_bild('anzeige','p_sh',k.src); } function zeige_level(nr) { set_input('knoeppe','form1','level',nr); } function zeige_punkte(nr) { set_input('knoeppe','form1','punkte',nr); } function zeige_feld(nr) { if (f[nr]<14) set_bild('feld','i_'+nr,ima[f[nr]].src); else set_bild('feld','i_'+nr,ima[f[nr]-10].src); } function zeige_leben(nr) { var i,k; for (i=0; i<pmax; i++) { if (i<nr) k=ima_p[2]; else k=ima[0]; set_bild('anzeige','p_'+i,k.src); } } function zeige_pfeile(nr) { var i,k; for (i=0; i<pfmax; i++) { if (i<nr) k=ima[5]; else k=ima[0]; set_bild('anzeige','pf_'+i,k.src); } }
Die beiden noch nicht erklärten Funktionen aus neu(flag)
bilden den Übergang zum nächsten Kapitel, sie sorgen endlich dafür, dass man die Sprites auch zu sehen bekommt.
function init_paccie() { if (pakt<0) // noch Reserven da? { aktiv=false; gestoppt=true; // Timer stoppen alert('Game over!'); kenn=jetzt_erlaubt; eintrag(punkte); kenn=wieder_dicht; } else with (pac) { kenn=mal_wieder_raten; speed=n_speed; phase=0; dx=speed; dy=0; z=8; // Paccie belegt Schicht 8 bild=ima_p[2]; ri=1; // nach rechts zustand=0; // lebt ix=start%xmax; // Feldkoordinate iy=Math.floor(start/xmax); // Feldkoordinate x=ofx+breite*ix; // Bildschirmkoordinate y=ofy+hoehe*iy; // Bildschirmkoordinate set_bild('l_pac','i_pac',bild.src); // Bild einsetzen set_koords('l_pac',x,y,z); // dem Layer Koordinaten verpassen show_hide('l_pac',true); // Layer anzeigen } zeige_leben(pakt); } function init_geist(nr) { with (kra[nr]) { speed=n_speed; phase=nr; dx=speed; dy=0; z=3+nr; // Kraken belegen Schicht 3-7 zustand=0; ix=nester[nr]%xmax; iy=Math.floor(nester[nr]/xmax); x=ofx+breite*ix; y=ofy+hoehe*iy; set_bild('l_kra'+nr,'i_kra'+nr,ima_k[nr].src); set_koords('l_kra'+nr,x,y,z); show_hide('l_kra'+nr,true); neue_krichtung(nr); } }
Die letzten zwei Funktionen der Crossbrowser-Routine, die hier noch fehlen, sind set_koords(layer,x,y,z)
und show_hide(layer)
. Mit Ausnahmen der Teile für NN4, die mit dem Recht des Ältesten ihr proprietäres Layer-Süppchen kochen, bedienen sie sich an Stylesheet-Elementen. IE4 als deren ältestes Modell genehmigt sich hier etwas andere Eigenschaften-Namen als die, die später von den W3-Konsorten abgesegnet und von Moz1.x verwendet werden:
Browser | Koordinatenzugriff | Ein/Ausschalten |
---|---|---|
NN4 | document.layers[lay].left=x document.layers[lay].top=y document.layers[lay].zIndex=z |
document.layers[lay].visibility='show' document.layers[lay].visibility='hide' |
IE4 | document.all[lay].style.pixelLeft=x document.all[lay].style.pixelTop=y document.all[lay].style.zIndex=z |
document.all[lay].style.visibility='visible' document.all[lay].style.visibility='hidden' |
NN5 W3C |
document.getElementById(lay).style.left=x document.getElementById(lay).style.top=y document.getElementById(lay).style.zIndex=z |
document.getElementById(lay).style.visibility='visible' document.getElementById(lay).style.visibility='hidden' |
Das Ganze wird umgesetzt zu:
function set_koords(lay,x,y,z) { if (my_browser=='ie4') { with (document.all[lay].style) { pixelLeft=x; pixelTop=y; zIndex=z; } } else if (my_browser=='nn4') { document.layers[lay].left=x; document.layers[lay].top=y; document.layers[lay].zIndex=z; } else with (document.getElementById(lay).style) { left=x; top=y; zIndex=z; } }
function show_hide(lay,flag) { var t; if (my_browser=='ie4') { if (flag) t='visible'; else t='hidden'; document.all[lay].style.visibility=t; } else if (my_browser=='nn4') { if (flag) t='show'; else t='hide'; document.layers[lay].visibility=t; } else { if (flag) t='visible'; else t='hidden'; document.getElementById(lay).style.visibility=t; } }
Die Bibliotheksfunktion set_koords(spritelayer,x,y,z)
schiebt die Sprites bzw. ihre Layers auf dem Bildschirm herum, show_hide(spritelayer,flag)
schaltet sie ein oder aus, set_bild(spritelayer,spriteimage,sprite-GIF)
tauscht das Sprite-Bild aus, den Rest erledigen Eigenschaften wie x,y,z,Breite,Höhe,Bewegung_x, Bewegung_y, Zustand u.s.w., die man dem Objekt sprite()
verpassen kann:
Im Großen und Ganzen kann man in Javascript Sprites mit Layern nachbilden, auch wenn solche Feinheiten wie IRQ bei Kollision und das Setzen einer Kollisionsmaske erst mal nicht nachgebildet werden können. Ja, und hardwaregesteuert wie auf gewissen Home- oder Kreativcomputern sind sie auch nicht. Aber dagegen haben auch Emulatoren zu kämpfen :-).
zum Seitenanfang | ||
zum Seitenende |
Bewegungen der Sprites vollziehen sich ganz einfach: man füttert das Objekt mit den neuen Werten (hier:Koordinaten) und lässt machen: set_koords(spritelayer,x,y,z)
. Dieses set_koords(layer,x,y,z)
wird in einen Funktion verpackt, die die neuen Sprite-Eigenschaften berechnet, diese wird zyklisch aufgerufen, fertig ist die Laube.
Die neuen Sprite-Koordinaten ergeben sich aus dem Sprite selbst:
with (pac) { x=x+dx; y=y+dy; z=z+dz; phase=(phase+1)%nphasen; set_koords('l_'+name,x,y,z); set_bild('l_'+name,'i_'+name,ima_p[nphasen*ri+phase].src); }
d.h. die Sprite-Bewegungen werden, abgesehen von Bild der jeweiligen Bewegungs-Phase, vollständig mit sprite.speed
, sprite.dx
und sprite.dy
gesteuert. Die Bewegungsphase wird eins hochgezählt, das entsprechende Bild angezeigt, that's all, folx!
move_all()
function move_all() { if (aktiv) { with (pac) if (zustand==0) { if (((x-ofx)%breite==0) && ((y-ofy)%hoehe==0)) neue_prichtung(); set_koords('l_pac',x,y,z); x=x+dx; y=y+dy; if ((phase%4)>1) set_bild('l_pac','i_pac',ima_p[2*ri+1].src); else set_bild('l_pac','i_pac',ima_p[2*ri].src); phase++; } for (var i=0; i<4; i++) with (kra[i]) if (speed>0) { if (((x-ofx)%breite==0) && ((y-ofy)%hoehe==0)) neue_krichtung(i); x=x+dx; y=y+dy; set_koords('l_kra'+i,x,y,z); } if (schussri>=0) check_schuss(); if (pac.zustand==0) check_crash(); } if (!gestoppt) timerec1=window.setTimeout("move_all()",50); }
aktiv
dient dazu, die Timer-Routine kurzfristig anzuhalten, die Funktion wird zwar weiterhin alle 50 Millisekunden aufgerufen, aber deren Anweisungen nicht ausgeführt. Brauchbar ist so was entweder für eine Pausenfunktion, oder wenn ein Teil ausgewertet wird, bei dem sich ein Weiterbewegen der Sprites tödlich für den Algorithmus auswirkt (Feld auswerten). Entgültig gestoppt, so dass man die move_all()
neu aufrufen muss, wird sie durch gestoppt=true
bei Levelende oder Spielende. Diese Umwege sind nötig, um zu vermeiden, dass aus Versehen zwei Instanzen dieser Funktion gleichzeitig ablaufen, und plötzlich alles doppelt so schnell wird.
Beim IE, NN4 und Opera geht das dann auch recht zügig über die Bühne, aber was haben sich die Programmierer von Mozilla1.x/NN5 gedacht? Das Layerschieben geht hier schleppend langsam über die Bühne, und eine Alternative nicht in Sicht! Seit Erscheinen von NN5 ist es besser, für solche Projekte Flash einzusetzen. Aber der Autor wollte zeigen, dass es auch mit normalen "Bordmitteln" wie Javascript geht, zumal die Spritesteuerung damit transparenter dargestellt werden kann!
Mögliche Abhilfe:
Da sich sprite.dx
und sprite.dy
aus sprite.speed
und sprite.ri
berechnen, kann man werden die NN5-Sprites mit einer ca. 33% höheren Geschwindigkeit füttern.
Sie sind dann genauso schnell wie die der anderen Browser, allerdings die Steuerung etwas weniger präzise. - Bei Bubbles(hooter) wurde diese Taktik auch angewandt, bei Paccie ließ ich es bei der geringeren Geschwindigkeit (auf Wunsch kann die andere Variante hochgeladen werden).
Zusatz am 28.12.2002
Die Mozilla-Version 1.2.1 ist in diesem Punkt endlich so stabil, dass man die Sonderbehandlung nicht mehr braucht. Dafür machen die (voll ausgefüllten Sprite-)Layers beim unabsichtlichen Markieren etwas Probleme, und es gibt zwar ein Kontextmenu "alles auswählen", aber nicht das Gegenstück "nichts auswählen".
Trotzdem ist es erfreulich, dass man auch mit Mozilla weiterhin Animationen via Javascript einigermaßen weich hinbekommt, so dass die Kategorie "Geschicklichkeit" erweitert werden kann.
zum Seitenanfang | ||
zum Seitenende |
Eine Richtungsänderung kann nur durchgeführt werden, wenn das Sprite ein Feld vollständig bedeckt, mit Ausnahme der Änderung in Gegenrichtung, die sofort erfolgen kann. Deshalb werden diese Fälle auch von verschiedenen Funktionen bearbeitet.
Als erstes muss bei Tastendruck also gecheckt werden, ob eine 180°-Wende ansteht.ri
ist so definiert, dass oben der 0
entspricht, 1
bedeutet rechts, 2
geht nach unten, 3
nach links. Eine Wende wäre also rechts/links oder oben/unten. Die Differenz ist absolut gesehen, immer 2.
wechsel()
function wechsel() { ok=(Math.abs(oldri-ri)==2); if (ok) { if (pac.dx==0) pac.dy=-pac.dy; else if (pac.dy==0) pac.dx=-pac.dx; oldri=ri; } return ok; }
Die obige Funktion kommt nur zum Einsatz für das Fischchen, wenn es über die Tastatur gesteuert wird.
Für die anderen Fälle gilt: ist ein Feld vollständig eingenommen, so gilt:(sprite.x-ofx)%sprite.breite==0) und (sprite.y-ofy)%sprite.hoehe==0)
Sind diese Bedingungen erfüllt, kann eine Richtungsänderung durchgeführt werden. Wohin? Das gibt die Bewegungsrichtung-Lookup-Tabelle an, die der Level-Editor berechnet hat.
Damit kann man jetzt die Kraken durch das Labyrinth laufen lassen.
Wir erinnern uns:
Wie sollen sich die Kraken jetzt bewegen? Zunächst mal muss auf die Torus-Welt reagiert werden, d.h.: laufen die Kraken links aus dem Feld, so sollen sie rechts auf gleicher Höhe wieder erscheinen. Es wird gecheckt, ob die Feldkoordinaten ix
und iy
noch im erlaubten Bereich liegen, sonst werden alle Koordinaten angepasst.
neue_krichtung(nr)
, Krakenbewegung in Torus-Welt// Bestimmung der Feldkoordinaten: kra[nr].ix=Math.floor((kra[nr].x-ofx)/kra[nr].breite); kra[nr].iy=Math.floor((kra[nr].y-ofy)/kra[nr].hoehe); // 4x Wrap-Around in der Torus-Welt: if (kra[nr].ix>=xmax) { kra[nr].ix=0; kra[nr].x=ofx; } else if (kra[nr].ix<0) { kra[nr].ix=xmax-1; kra[nr].x=kra[nr].ix*kra[nr].breite+ofx; } if (kra[nr].iy>=ymax) { kra[nr].iy=0; kra[nr].y=ofy; } else if (kra[nr].iy<0) { kra[nr].iy=ymax-1; kra[nr].y=kra[nr].iy*kra[nr].hoehe+ofy; }
Als nächstes müssen Aktionen bedacht werden, die durch das Betreten des neuen Feldes ausgelöst werden. Bei den Kraken sind dies die Geschwindigkeitsänderungen:
neue_krichtung(nr)
, Reaktion auf Feldinhalte// Reaktion auf Feldinhalte: temp=kra[nr].iy*xmax+kra[nr].ix; // Feldkoordinate if (f[temp]==7) // Kugel und Kette { kra[nr].speed=l_speed; f[temp]=0; // kann man löschen, muss man nicht zeige_feld(temp); } else if (f[temp]==8) // Blitz { kra[nr].speed=h_speed; f[temp]=0; // kann man löschen, muss man nicht zeige_feld(temp); } else if (f[temp]==9) // Normal { kra[nr].speed=n_speed; f[temp]=0; // kann man löschen, muss man nicht zeige_feld(temp); }
Ist dies alles erledigt, so kann eine neue Richtung gewählt werden. Um sich lange Suchereien zu ersparen, wo man überhaupt hinkann, haben wir vom Level-Editor die Bewegungs-Lookup-Tabellen erstellen lassen. Wir brauchen hier die fk
für die Kraken.
Nun sollte man sich Gedanken über die Bewegungen der Kraken machen. Ich habe hier folgende Regeln aufgestellt:
Wie schon gesagt, Punkt 2 - 4 sind Geschmackssache, und ich möchte niemanden vom Experimetieren abhalten. Die obigen Regeln führen zu folgendem Code:
frei=fk[temp]; // Wert aus Lookup-Tabelle if (frei==1) // nur oben frei { kra[nr].dx=0; kra[nr].dy=-kra[nr].speed; } else if (frei==2) // nur rechts frei { kra[nr].dy=0; kra[nr].dx=kra[nr].speed; } else if (frei==3) // oben oder rechts, nicht umkehren! { if (kra[nr].dx!=0) // Krake bewegt sich horizontal, { kra[nr].dx=0; // neue Richtung: oben kra[nr].dy=-kra[nr].speed; } else // Krake bewegt sich vertikal { kra[nr].dy=0; // neue Richtung: rechts kra[nr].dx=kra[nr].speed; } } else if (frei==4) // nur nach unten frei { kra[nr].dx=0; kra[nr].dy=kra[nr].speed; } else if (frei==6) // unten und rechts frei, nicht umkehren! { if (kra[nr].dx!=0) // Krake bewegt sich horizontal, { kra[nr].dx=0; // neue Richtung: unten kra[nr].dy=kra[nr].speed; } else // Krake bewegt sich vertikal { kra[nr].dy=0; // neue Richtung: rechts kra[nr].dx=kra[nr].speed; } } else if (frei==7) // oben, unten, rechts { if (kra[nr].dx==0) // Krake bewegt sich vertikal { if (Math.random()<0.5) // soll mit 50% W abbiegen { kra[nr].dx=kra[nr].speed; kra[nr].dy=0; } } else // Krake bewegt sich derzeit horizontal, keine Umkehr, // gleiche Wahrscheinlichkeit für oben oder unten { kra[nr].dx=0; if (Math.random()<0.5) kra[nr].dy=kra[nr].speed; else kra[nr].dy=-kra[nr].speed; } } else if (frei==8) // nur links frei { kra[nr].dy=0; kra[nr].dx=-kra[nr].speed; } else if (frei==9) // links und oben { if (kra[nr].dx==0) { kra[nr].dy=0; kra[nr].dx=-kra[nr].speed; } else { kra[nr].dx=0; kra[nr].dy=-kra[nr].speed; } } else if (frei==11) // oben, rechts, links { if (kra[nr].dy==0) // horizontale Bewegung { if (Math.random()<0.5) // abbiegen? { kra[nr].dx=0; kra[nr].dy=-kra[nr].speed; } } else // vertikal { kra[nr].dy=0; // nach rechts oder links if (Math.random()<0.5) kra[nr].dx=kra[nr].speed; else kra[nr].dx=-kra[nr].speed; } } else if (frei==12) // links, unten { if (kra[nr].dy==0) // links nach unten { kra[nr].dx=0; kra[nr].dy=kra[nr].speed; } else // unten nach links { kra[nr].dy=0; kra[nr].dx=-kra[nr].speed; } } else if (frei==13) // oben, unten, links { if (kra[nr].dx==0) // vertikal { if (Math.random()<0.5) // Abbiegen? { kra[nr].dx=-kra[nr].speed; kra[nr].dy=0; } } else // horizontal { kra[nr].dx=0; // oben oder unten if (Math.random()<0.5) kra[nr].dy=kra[nr].speed; else kra[nr].dy=-kra[nr].speed; } } else if (frei==14) // rechts, links, unten { if (kra[nr].dy==0) // horizontal { if (Math.random()<0.5) // Abbiegen? { kra[nr].dx=0; kra[nr].dy=kra[nr].speed; } } else // vertikal { kra[nr].dy=0; // rechts oder links if (Math.random()<0.5) kra[nr].dx=kra[nr].speed; else kra[nr].dx=-kra[nr].speed; } } else if (frei==15) / Kreuzung, anything goes: { if (Math.random<0.05) // Umkehr { kra[nr].dy=-kra[nr].dy; kra[nr].dx=-kra[nr].dx; } else if (Math.random()<0.15) // Drehung rechts { i=kra[nr].dy; kra[nr].dy=kra[nr].dx; kra[nr].dx=i; } else if (Math.random()<0.25) // Drehung links { i=kra[nr].dy; kra[nr].dy=-kra[nr].dx; kra[nr].dx=-i; } } // Patch für Krakenstart aus Nestern, da Werte meist unpassend initialisiert: else if ((frei==5) && (kra[nr].dx!=0)) // unpassender Wert { kra[nr].dx=0; // rauf oder runter if (Math.random()<0.5) kra[nr].dy=-kra[nr].speed; else kra[nr].dy=kra[nr].speed; } else if ((frei==10) && (kra[nr].dy!=0)) // unpassender Wert { kra[nr].dy=0; // rechts oder links if (Math.random()<0.5) kra[nr].dx=-kra[nr].speed; else kra[nr].dx=kra[nr].speed; }
Da die Krakenrichtung nicht vom Leveleditor vorgegeben wird, ist die derzeitige die Richtunng der Krake die aus dem vorigen Level, also irgend eine Zufallsrichtung. Dies ist meist ein unbrauchbarer Werte für den Start aus einem Krakennest, so dass die Krake zu Beginn eventuell durch eine Mauer läuft. Wenn die Krake also aus ihrem Nest startet, muss fbei waagerechten und senkrechten Gang explizit die Richtung gewählt werden.
Die Bewegungen des Fischchens werden ähnlich codiert, nur dass der nicht seine neue Richtung selber wählt, sondern bei Erreichen einer Mauer in seiner Bewegung stoppt (dx=0, dy=0), bis ihn ein Tastendruck aus der Untätigkeit erlöst und ihm eine neue Richtung gibt. Natürlich muss er auf noch mehr Feldelemente wie Punkte, Kraftpillen, Pfeile, Goodies und Höhlen reagieren, aber das sollte kein Problem mehr sein, da es nach dem selben Strickmuster wie die Geschwindigkeitsänderung der Kraken geht.
In die Paccie-Bewegungsfunction streuen wir aber auch Goodies ein und werten den Bewegungs-Counter tic
aus, der bestimmt, wie lange Kraken auf "Freeze" reagieren.
neue_prichtung()
function neue_prichtung() { var i,frei,temp,ok=false; // neues Feld: pac.ix=Math.floor((pac.x-ofx)/pac.breite); pac.iy=Math.floor((pac.y-ofy)/pac.hoehe); if (pac.ix>=xmax) { pac.ix=0; pac.x=ofx; } else if (pac.ix<0) { pac.ix=xmax-1; pac.x=pac.ix*pac.breite+ofx; } if (pac.iy>=ymax) { pac.iy=0; pac.y=ofy; } else if (pac.iy<0) { pac.iy=ymax-1; pac.y=pac.iy*pac.hoehe+ofy; } // Feldkoordinate: temp=pac.iy*xmax+pac.ix; if (temp==start) goodies(); // Leckerchen am Startplatz kassieren else if (f[temp]==2) // einfache Punkte, die gefressen werden müssen { punkte++; zeige_punkte(punkte); f[temp]=0; zeige_feld(temp); anz_chips--; if (anz_chips<1) nextlevel(); } else if (f[temp]==3) // Kraftpille { f[temp]=0; zeige_feld(temp); geisterfang(); } // Höhle else if ((f[temp]==6) && (pac.dx+pac.dy!=0)) temp=transloc(temp); else if (f[temp]==7) // Kugel und Kette { change_speed(pac,1); f[temp]=0; zeige_feld(temp); } else if (f[temp]==8) // Blitz { change_speed(pac,2); f[temp]=0; zeige_feld(temp); } else if (f[temp]==9) // normal { change_speed(pac,0); f[temp]=0; zeige_feld(temp); } else if ((f[temp]==15) && (pfakt<pfmax)) // Pfeil aufsammeln { pfakt++; zeige_pfeile(pfakt); f[temp]=0; zeige_feld(temp); } // - ab hier neue Richtung bestimmen: frei=fp[temp]; // Wert aus Lookup-Tabelle für Paccie if (ri==0) ok=(frei%2>0); else if (ri==1) ok=(frei%4>1); else if (ri==2) ok=(frei%8>3); else if (ri==3) ok=(frei%16>7); set_bild('l_pac','i_pac',ima_p[2*ri].src); if (ok) { if (ri==0) { pac.dx=0; pac.dy=-pac.speed; } else if (ri==1) { pac.dx=pac.speed; pac.dy=0; } else if (ri==2) { pac.dx=0; pac.dy=pac.speed; } else if (ri==3) { pac.dx=-pac.speed; pac.dy=0; } } else // ausbremsen: { pac.dx=0; pac.dy=0; } pac.phase=0; if (Math.random()<0.01) // Bonbon am Start { goodie=Math.floor(5*Math.random())+10; set_bild('feld','i_'+start,ima[goodie].src); } if (level>2) if (Math.random()<0.02) // ab Level3 gibt es Pfeile { temp=Math.floor(zmax*Math.random()); if (f[temp]==0) { f[temp]=15; zeige_feld(temp); } } // ab Level 5 gibt es Kugeln, Blitze und für Paccie Normalspeed if (level>4) if (Math.random()<0.02) { temp=Math.floor(zmax*Math.random()); if (f[temp]==0) { f[temp]=7; zeige_feld(temp); } } if (level>4) if (Math.random()<0.02) { temp=Math.floor(zmax*Math.random()); if (f[temp]==0) { f[temp]=8; zeige_feld(temp); } } if (level>4) if (Math.random()<0.02) { temp=Math.floor(zmax*Math.random()); if (f[temp]==0) { f[temp]=9; zeige_feld(temp); } } // Kraken 50 Tics nach Freeze wieder lösen: if (tic>0) { tic--; if (tic<1) for (i=0; i<nester[0]; i++) kra[i].speed=n_speed; } }
zum Seitenanfang | ||
zum Seitenende |
Wenn der Pacfisch ein Feld vollständig einnimmt, so soll er dessen Leckerchen ernten. Das Feld ist anschließend wieder leer.
goodies()
function goodies() { var i; if (goodie==10) // Leben { if (pakt<pmax) { pakt++; zeige_leben(pakt); } } else if (goodie==11) // Extrapunkte { punkte=punkte+100; zeige_punkte(punkte); } else if (goodie==12) nextlevel(); else if (goodie==13) { shooter=true; zeige_shooter(shooter); } else if (goodie==14) // Eisberg, Freeze, Kraken sind eingefroren { for (i=0; i<nester[0]; i++) kra[i].speed=0; tic=50; // 50 Tics Freeze } goodie=0; set_bild('feld','i_'+start,ima[0].src); }
Tritt das Fischchen auf eine Höhle, so taucht es in der nächsten in der Liste wieder auf. Verschwindet es in der letzten, so taucht es in der ersten auf (Höhlenring).
transloc(nr)
function transloc(nr) { var i,k=-1; if (h_liste[0]>0) { // die nächste Höhle ist die nächste in der h_liste: for (i=1; i<=h_liste[0]; i++) if (h_liste[i]==nr) k=i+1; // nach der Höhle mit dem höchsten Index ist 1 wieder dran if (k>h_liste[0]) k=1; k=h_liste[k]; // -- und Sprung! pac.ix=k%xmax; pac.iy=Math.floor(k/xmax); pac.x=pac.ix*pac.breite+ofx; pac.y=pac.iy*pac.hoehe+ofy; set_koords('l_pac',pac.x,pac.y,pac.z); pac.dx=0; pac.dy=0; pac.phase=0; } return k; }
Bliebe noch die Geschwindigkeitsänderung:
change_speed(sprite,modus)
function change_speed(sprit,modus) { if (modus==0) sprit.speed=n_speed; else if (modus==1) sprit.speed=l_speed; else sprit.speed=h_speed; }
zum Seitenanfang | ||
zum Seitenende |
Wir haben bisher die Bewegungsfunktionen mit Neuberechnung der Richtungen, nun muss das Ganze in eine kontinuierliche
Bewegung umgesetzt werden. Dazu muss die Funktion move_all()
zeitgesteuert aufgerufen werden.
Eine Möglichkeit ist, am Ende dieser Funktion ein window.setTimeout("move_all()",50)
einzubauen.
Das nächste Anstoßen der Funktion geschähe dann in 50 Millisekunden.
Mit dem Flag aktiv
wird gesteuert, ob die Anweisungen/Bewegungen ausgeführt werden oder nicht. Damit sich move_all()
bei aktiv=false
nicht ganz ausschaltet, wird nur die Ausführung der weiteren Anweisungen umgangen.
Es empfiehlt sich, bei zeitintensiven Funktionen wie z.B. Rekursionen aktiv
erst mal auf false
zu setzen, dann nach vollständigem Abarbeiten wieder auf true zu setzen, so dass praktisch während der Zeit nur eine
"leere" Zeitschleife läuft. So wird es auch von den Tastaturfunktionen verwendet.
move_all()
function move_all() { if (aktiv) { // einen Haufen Anweisungen, // was zeitgesteuert zu tun ist, // komplette Funktion: s.Kap.4.2.1 } if (!gestoppt) window.setTimeout("move_all()",50); }
Damit die zeitgesteuerte Funktion aber auch ganz abgeschaltet werden kann, wird die Variable gestoppt
eingeführt und mit true
initialisiert (zu Beginn ist alle Bewegung gestoppt). Die Funktion
neu(flag)
startet nach Initialisieren von Spielfeld und Anzeigen dann das Ganze.
gestoppt
ist true
nach Beenden eines Levels, Gameover oder "out of levels".
Wichtig! Es muss auf jeden Fall verhindert werden, dass move_all()
durchlaufen wird, bevor
es mit neu()
neu angestoßen wird. Quick and dirty geschieht dies durch zeitversetzten Aufruf
von neu()
, wobei der Zeitpuffer großzügig bemessen wird.
Es reicht nicht, die Variable gestoppt
abzufragen, diese kann bereits auf true
stehen,
während der if-Zweig, der die Funktion move_all()
neu startet, schon angelaufen ist. Dies ist beim Testen
häufig genug passiert: die Sprites rasen im neuen Level mit doppelter Geschwindigkeit, weil 2 Timerfunctions laufen!
Daher die zusätzliche Sicherheit durch die halbe Sekunde Verzögerung in nextlevel()
. Die halbe Sekunde
Verschnaufpause zwischen den Levels ist eine angenehme Nebenerscheinung.
nextlevel()
function nextlevel() { aktiv=false; gestoppt=true; // Timer abschalten level++; punkte=punkte+1000*level; zeige_punkte(punkte); zeige_level(level); // Safety first, aber eine halbe Sekunde Puffer ist großzügig: if (level<=maxlevels) window.setTimeout("neu(false)",500); else { punkte=punkte+punkte; zeige_punkte(punkte); alert('Mehr Levels hammanich!'); kenn=jezz_darfse; eintrag(punkte); kenn=jezz_nichmehr; } }
Die Sprites huschen nach Neustart durch die Gänge, die Kraken wählen selbständig neue Wege nach vorgegebenen Regeln und reagieren auf die für sie relevanten Feldelemente. Die Reaktion des Fisches auf die Feldelemente lässt sich analog programmieren. Die "Leckerchen" lassen sich zufallsgesteuert ins Spielfeld einstreuen, Zum Spielen fehlt eigentlich nur noch die Paccie-Steuerung und die Reaktion auf den Zusammenstoß Paccie-Krake.
zum Seitenanfang | ||
zum Seitenende |
Wann stoßen zwei Sprites zusammen? - Wenn mindestens je 1 Pixel ihrer Kollisionsmaske die gleiche Koordinate haben!
Nein, so gut haben wir es nicht! Wir arbeiten mit Annäherungen:
Mit diesen Annäherungen lässt es sich ganz gut leben, 1. gilt für Kugeln(Bubbles), 2 für rechteckige Sprites (Paccie, Kraken). Und so wird es gemacht:
function check_crash() { for (var i=0; i<nester.length; i++) if (kra[i].zustand<=50) { if ((Math.abs(pac.y-kra[i].y)<pac.hoehe) && (Math.abs(pac.x-kra[i].x)<pac.breite)) crash(i); } }
crash(nr)
erledigt dann die Reaktion auf den Zusammenstoß. Können die Kraken gefressen werden (Zustand zwischen 10 und 50) so gibt es Punkte, und die Krake ist erst mal erledigt (Zustand=100). Die erste bringt 50, bei jeder weiteren verdoppelt sich der Bonus. Ansonsten: ist die Krake gesund, wird das Bärschlein vernascht!
function crash(nr) { var i,k; if ((kra[nr].zustand>10) && (kra[nr].zustand<=50)) { kra[nr].zustand=100; set_bild('l_kra'+nr,'i_kra'+nr,ima_k[5].src); k=50; for (i=0; i<nester[0]; i++) if (kra[i].zustand==100) k=k+k; punkte=punkte+k; zeige_punkte(punkte); } else if (kra[nr].zustand<1) // armes Bärschlein! { pac.zustand=8; pakt--; kenn=verwirrspiel; set_bild('l_pac','i_pac',ima_p[pac.zustand].src); init_paccie(); } }
Paccie und Krake sind aber nicht die einzigen Sprites, die aufeinandertreffen können. Da sind auch noch die Pfeile, mit denen man die Kraken erschießen kann. Die Pfeile enden in einer Krake oder an einer Mauer. Und wenn in der Schussrichtung keine Mauer liegt (Torus-Welt)? Dann kann kein neuer Schuss abgegeben werden, bis sich eine unvorsichtige Krake aufspießen lässt! Auch in einer Torus-Welt sollte man aufpassen, wohin man schießt :-).
function schuss() { // Pfeil und Harpune vorhanden? // Wenn kein weiterer Pfeil unterwegs ist, // Abschuss in dieselbe Richtung, wie Paccie schwimmt if ((pfakt>0) && (shooter)) if (schussri<0) { schussri=ri; set_bild('l_pfe','i_pfe',ima_pf[schussri].src); pfe.ix=pac.ix; pfe.iy=pac.iy; pfe.x=pfe.ix*pfe.breite+ofx; pfe.y=pfe.iy*pfe.hoehe+ofy; set_koords('l_pfe',pfe.x,pfe.y,pfe.z); show_hide('l_pfe',true); pfakt--; // nach Schuss 1 Pfeil weniger zeige_pfeile(pfakt); } } function check_schuss() { var i,xneu,yneu,p; // Torus-Welt: if (schussri==0) { xneu=pfe.ix; yneu=(pfe.iy+ymax-1)%ymax; } else if (schussri==1) { yneu=pfe.iy; xneu=(pfe.ix+1)%xmax; } else if (schussri==2) { xneu=pfe.ix; yneu=(pfe.iy+1)%ymax; } else if (schussri==3) { yneu=pfe.iy; xneu=(pfe.ix+xmax-1)%xmax; } p=yneu*xmax+xneu; // ----- Hammer was getroffen? ----- if (((f[p]!=0) && (f[p]!=2)) && ((f[p]!=3) && (f[p]!=5))) // Mauer { schussri=-1; show_hide('l_pfe',false); } else // Pfeil fliegt 1 Feld weiter: { pfe.ix=xneu; pfe.iy=yneu; set_koords('l_pfe',pfe.ix*pfe.breite+ofx,pfe.iy*pfe.hoehe+ofy,pfe.z); // Krake erwischt? for (i=0; i<nester.length; i++) if (kra[i].zustand<=50) { if (pfe.ix==kra[i].ix) if (Math.abs(pfe.y-kra[i].y)<pfe.hoehe) { kra[i].zustand=50; // --- Halali! crash(i); schussri=-1; } if (pfe.iy==kra[i].iy) if (Math.abs(pfe.x-kra[i].x)<pfe.breite) { kra[i].zustand=50; // --- Halalo! crash(i); schussri=-1; } } } }
zum Seitenanfang | ||
zum Seitenende |
Pacfish wäre kein echter Paccie-Clone, wenn man nicht wie gewohnt, den Pac mit den Tasten durch das Labyrinth steuern kann. Es gab Versuche mit Maussteuerung, aber alle endeten in Spielerfrust. Also muss eine Tastaturauswertung her. Der einfachste Weg ist, die Tasten in einem Input-Formfeld auslesen zu lassen. Den wäre ich vor lauter Verzweiflung auch für Opera gegangen, aber das Spielfeld sieht dadurch extrem uncool aus!
Zunächst mal müssen wir die Browser dazu bringen, Bescheid zu geben, wenn eine Taste gedrückt wurde.
Im NN4 erledigt das document.captureEvents(event1|event2|event3|...)
, wir brauchen hier nur
Event.KEYDOWN
, Event.KEYPRESS
oder Event.KEYUP
, je nach gewünschter Steuerung.
Nach document.captureEvents(Event.KEYUP)
wird also Bescheid gesagt, wenn eine Taste losgelassen wurde. Ab jetzt haben wir ein Objekt document.onkeyup
, dem wir eine Funktion unterjubeln können, die jedesmal ausgeführt wird, wenn eine Taste gedrückt wird. Zu beachten ist, dass beim Initialisieren die Funktion *keine* Funktionsklammern enthält!
Und was ist nun mit IE und dem Rest der Welt? - Große Freude, onkeyup
und ähnliches ist ab IE4 und spätestens seit HTML4 im body
(u.v.a.) erlaubt!
taste_auswerten()
ans KEYUP-Eventfunction init_key() { if (document.captureEvents) document.captureEvents(Event.KEYUP); document.onkeyup=taste_auswerten; // keine Funktionsklammern!!! }
Diese Funktion wird am besten zu Beginn des Spiels einmal aufgerufen, ein guter Platz dafür ist <body onload=".....">
.
Hier haben wir wieder 3 Wege, die beschritten werden müssen.
window.event
"nachgefasst" werden.which
ab, IE4-6 in keyCode
document.getElementById
. Hier landen alle W3C-kompatiblen Browser.
Konsortium und neueste Browser sind sich aber noch uneinig, was Tastaturcodes angeht.
Also wird erst mal alles abgefragt und gehofft, dass der Browser eins davon versteht:
bisher aufgetauchten Event-Eigenschaften sind which
, charCode
und keyCode
,
damit sind IE und NN abgedeckt. Mehr ist zur Zeit nicht drin,
aber das ist laut Logfile-Analyse nach Abzug der Suchmaschinen und Sauger schon >99%! Und Opera? Siehe Kap. 5.4.!
taste_auswerten(KEYUP.event)
function taste_auswerten(evt) { var my_cc; var c = evt || window.event; // NN4 || Rest if (document.layers) my_cc=c.which; // NN4 else if (document.all) my_cc=c.keyCode; // IE4 else if (document.getElementById) // W3C { if (c.charCode>0) my_cc=c.charCode; // ASCII-Code bei keypress else if (c.which>0) my_cc=c.which; else if (c.keyCode>0) my_cc=c.keyCode; else my_cc=-1; // Browser kann nicht } else my_cc=-1; // es liegt nix an if (aktiv) abmarsch(my_cc); // falls aktive Spielphase, Code auswerten }
Die 3.Zeile ist eine (bitgesteuerte) Kurzform für
if (evt) c=evt; else c=window.event;
Schon fertig? Nein, die verschiedenen Eigenschaften liefern unterschiedlichen Code.
Da wir auf die Cursortasten erpicht sind, steht uns noch ein bisschen Arbeit bevor.
Bei so vielen Wegen und keinem Wegweiser a la W3C muss man das Beste aus den browserischen Gegebenheiten machen.
Bevorzugte Tasten sind erst mal die Pfeiltasten, danach die des numerischen Keypads, und wenn die alle nicht
funktioklappern, die Tasten w für oben, x oder y für unten, j für links und k für rechts.
Bei Ralf Beutler gibt es ein nettes Tool, um die Codes zu erhalten:
TastaturEvents -
Dabei ergibt sich:
Browser | Tasten | angelieferte Codes oben,unten,rechts,links |
---|---|---|
NN4 | num.Keypad w,(xy),k,j | 56,52,50,54 119,(120,121),107,106 |
IE NN5 | Cursors num.Keypad w,(xy),k,j |
38,40,39,37 98,104,102,100 87,(88,89),75,74 |
OP6 | w,(xy),k,j | 87,(88,89),75,74 |
Die Programmierer von Opera6 hatte den glorreichen Einfall, auf die Zahlen Sonderfunktionen zu legen. Damit ist das num.Keypad für die Steuerung unbrauchbar.
Die Werte für die Steuerung über Zahlen liegen um 56 auseinander, die für die Steuerung übers Alphabet um 32. Also ziehen wir die entsprechenden Werte ab und müssen nur 3 Zahlen pro Richtung berücksichtigen. Dann wäre da noch der Schuss bei der Leertaste (32), und fertig:
abmarsch(gelieferter_code)
// ---------------------------------------------- // Tastaturcodes, Einbinde-Teil: ---------------- // ---------------------------------------------- function abmarsch(c) { // erst mal keine weiteren Events verarbeiten: aktiv=false; // Cursor-Keys: 38,39,40,37 // num. Keypad: 56,50,52,54 - 98,102,104,100 (delta=56) // w,k,(x,y),j: 87,75,(88,89),74 - 119,106,(120,121),107 (delta=32) if (c>96) c=c-32; else if (c>90) c=c-56; // Cursor und num.Keypad abfragen: if ((c==56) || (c==38)) { oldri=ri; ri=0; } if ((c==54) || (c==39)) { oldri=ri; ri=1; } else if ((c==50) || (c==40)) { oldri=ri; ri=2; } else if ((c==52) || (c==37)) { oldri=ri; ri=3; } // fuer Operaner und NN4: w,k,xy,j: else if (c==87) { oldri=ri; ri=0; } else if (c==75) { oldri=ri; ri=1; } else if (c==74) { oldri=ri; ri=3; } else if (c==88) { oldri=ri; ri=2; } else if (c==72) { oldri=ri; ri=2; } // Schießen mit Leertaste: else if (c==32) schuss(); // Richtung ändern, falls neue Richtung angegeben if (ri!=oldri) wechsel(); // und Eventverarbeitung wieder aktiv schalten: aktiv=true; } // ----------------------------------------------
Damit sind alle Einzelteile fertig, um ein schickes Spiel zu basteln, was nicht unbedingt auf Pacman basieren muss: ein Spiele-Hintergrund, Levels, Sprites, Zusammenstoß Sprite-Hintergrund und Sprite-Sprite, Maussteuerung und Tastatursteuerung. Der alte C64 liefert genug Vorlagen!
zum Seitenanfang | ||
zum Seitenende |