"Kann der auch Sprites?" - Pacfish mit Leveleditor

... 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.

Inhalt

  1. Die Grafiken
    1. Pacfish-Bewegungsphasen
    2. Krakenzustände
    3. Pfeile, Futter- und Kraftpillen
    4. Levelgestaltung: Hintergrund, Mauer, Tunnels, Kraken-Startpunkte
    5. Bonusse und Malusse

  2. Level-Editor
    1. Was muss der Level-Editor können?
    2. Wie sollen die Levels übergeben werden?
    3. Layout des Level-Editors
    4. Funktionen des Level-Editors

  3. Das Spielfeld
    1. Aufbau des Spielfeldes
    2. Anzeigen
    3. Zusammensetzen des HTML-Teils

  4. Sprites
    1. Definition der Sprites
    2. Bewegungen der Sprites und Auswerten der Felder
      1. Grundsätzliches
      2. Richtungsänderung
      3. Felder auswerten
    3. Timerüberlegungen
    4. Interaktion mit anderen Sprites

  5. Tastatursteuerung
    1. Events abfangen
    2. Keys auswerten
    3. Ausführen von Richtungsänderungen, Browserprobleme


1. Die Grafiken

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).

1.1. Pacfish-Phasen

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.
Pacfish-Phasen Fazit: wir brauchen 8 Fische, von denen 2 gezeichnet werden müssen, die anderen erhält man durch Drehen und Spiegeln der 2 Ursprungszeichnungen:
toter Pacfish Halt! Eine Phase haben wir vergessen: den toten Pacfish! Sind also insgesamt 9, die zweckmäßigerweise nach den Richtungen und Phasen benannt werden.

1.2. Krakenzustände

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.
Krakenphasen 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.

1.3. Pfeile,Futter- und Kraftpillen

Bonusse unterwegs 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.

1.4. Levelgestaltung: Hintergrund, Mauer, Tunnels, Kraken-Startpunkte

Levelgestaltung 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.

1.5. Bonusse und Malusse

Blieben noch die Goodies, die an dem Platz erscheinen, an dem der Paccie startet. Implementiert wurden:

Bremser/Beschleuniger 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.

2. Level-Editor

2.1. Was muss der Level-Editor können?

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.

2.2. Wie sollen die Levels übergeben werden?

... na, möglichst schnell und kompakt! :-)

Die Levels des Spiels sind abgelegt in einem Feld der Dimension xmax*ymax, dabei bedeutet

  1. Feld ist leer
  2. Feld ist mit Stein versperrt
  3. zu fressender Punkt
  4. Kraftpille
  5. Krakenstart
  6. Paccie-Start und Goodie-Platz
  7. Höhle

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.

Inhalt der Datei 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:

Inhalt der Datei levels.js, II. Erzeugen der Felder
  h_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:

2.3. Layout des Level-Editors

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.

Editor: HTML-Teil
<!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.

Die einzelnen Zellen für das Spielfeld bestehen aus verweissensitiven Grafiken und sollten in HTML folgendes Aussehen haben, am besten alles in eine Zeile, um unschöne Ränder zu vermeiden:

...
<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

Funktion 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>
...
Funktion 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;
}

2.4. Funktionen des Level-Editors

Was macht so ein Level-Editor?

  1. initialisieren
  2. laden
  3. ändern
  4. speichern

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'; }
2.4.1. Initialisieren

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
}
2.4.2. vorhandene Felder laden

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.

2.4.3. Level bearbeiten

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:

Ich bin letzteren Weg gegangen, der allerdings verlangt, dass 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); }
2.4.4. Level abspeichern

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.

Vorbereitung in 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:

Nachbarn in einer Torus-Welt

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!

Erzeugen der Lookup-Tabellen für die Bewegungen
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;
  }
}
Funktion 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:

Funktion 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.

3. Das Spielfeld

3.1. Aufbau

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:

Bilder vorladen
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:

Layer-Erzeugung in NN4 und dem Rest der Welt
 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 layers oder divs verpackt, dann hingeschrieben. Dieses Layer/Div erscheint sichtbar an den Browserkoordinaten y=40/x=24/z=1 und haben eine Breite von 388 Pixeln.

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:

BrowserErsetzen eines Bildes im Layer
NN4document.lay1.document.ima1.src=bildname
document.layers[my_layer].document.images[my_bild].src=bildname
IE4document.all.ima1.src=bildname
document.all[my_bild].src=bildname
NN5document.images[my_bild].src=bildname
W3Cdocument.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:

Crossbrowser-Bibliothek: Ersetzen eines Bildes in einem Layer
// 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
}

3.2. Anzeigen und Steuerbuttons

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..

Code zum Erzeugen der Anzeigen Leben, Harpune, Pfeile
// 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:

Crossbrowser-Bibliothek: Ersetzen des Textes eines Input-Elementes in einem Layer
// 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
}

3.3. Zusammensetzen des HTML-Teils

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.

4. Sprites

4.1. Definition der Sprites

Ein Sprite ist ein nettes, kleines verschiebbares Bildchen, was bei Kollision mit einem anderen (oder dem Hintergrund, je nach Betriebssystem und Sprache) einen Interrupt auslöst. Am ähnlichsten kommt dem ein Layer mit einem Image. Nur mit dem Auslösen eines IRQ hapert es. Also müssen wir noch etwas Hand anlegen. Das Sprite sollte haben:

  1. einen Namen, um es anzusprechen
  2. ein Bildchen
  3. Koordinaten (x,y,z)
  4. Geschwindigkeitsvektoren (dx,dy,dz)
  5. einen Zustand (ok,krank,gefressen,tot)
  6. Zweckmäßig sind hier noch folgende Parameter:
  7. Phase der Bewegung
  8. Breite, Höhe
  9. Koordinaten bezogen auf die Feldelemente
Erzeugen von Sprites in Javascript
// 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:

Funktion 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:

Bibliotheks-Interfaces
// 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:

BrowserKoordinatenzugriffEin/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:

Crossbrowser: Koordinaten eines Layers setzen
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;
  }
}
Crossbrowser: Sprite erscheinen/verschwinden lassen
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 :-).

4.2. Bewegung der Sprites

4.2.1. Grundsätzliches

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!

Timergebundene Funktion 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.

4.2.2. Richtungsänderungen

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.

Funktion 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:

  1. oben frei, Sackgasse
  2. rechts frei, Sackgasse
  3. oben und rechts frei, Gang
  4. unten frei, Sackgasse
  5. oben und unten frei, Gang
  6. rechts und unten frei, Gang
  7. oben, unten und rechts frei, Gang mit Abzweigung
  8. links frei, Sackgasse
  9. oben und links frei, Gang
  10. links und rechts frei, Gang
  11. oben, links und rechts frei, Gang mit Abzweigung
  12. unten und links frei, Gang
  13. oben, unten und links frei, Gang mit Abzweigung
  14. links, rechts und unten frei, Gang mit Abzweigung
  15. alle 4 Richtungen frei, Kreuzung

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.

Auszug aus 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:

Auszug aus 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:

Regeln für die Eigenbewegung der Kraken
  1. bei nur einer Möglichkeit (Sackgasse) wird diese genommen
    (Lookup-Tabellenwerte: 1,2,4,8)
  2. Bei 2 Möglichkeiten (Gang) soll keine Umkehr erfolgen. Dies ist Geschmackssache, ich kann mir durchaus vorstellen, dass man in etwa 10% der Fälle die Kraken umkehren lässt. Der Algo ist dann entsprechend anzupassen.
    (Lookup-Tabellenwerte: 3,5,6,9,10,12)
  3. Bei 3 Möglichkeiten (Gang mit Abzweigung) soll keine Umkehr erfolgen, dafür mit gleicher Wahrscheinlichkeit eine der beiden anderen Richtungen eingeschlagen werden.
    (Lookup-Tabellenwerte: 7,11,13,14)
  4. Bliebe noch die Kreuzung: Hier lasse ich 5% Umkehr zu, in 15% aller Fälle dreht sich die Krake im Uhrzeigersinn, in 25% der Fälle gegen den Uhrzeigersinn, sonst läuft sie geradeaus.
    (Lookup-Tabellenwert: 15)

Wie schon gesagt, Punkt 2 - 4 sind Geschmackssache, und ich möchte niemanden vom Experimetieren abhalten. Die obigen Regeln führen zu folgendem Code:

Neue Richtung der Kraken: Auswerten Lookup-Tabelle
  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.

vollständige Funktion 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;
  }
}
4.2.3. Felder auswerten

Wenn der Pacfisch ein Feld vollständig einnimmt, so soll er dessen Leckerchen ernten. Das Feld ist anschließend wieder leer.

Bonusse abräumen: 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).

Reaktion auf Höhlen 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; 
}

4.3. Timerüberlegungen

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.

Steuerung der Funktion 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.

Funktion 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.

4.4. Interaktion mit anderen Sprites

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:

  1. Wenn ihre Mitten nicht weiter voneinander entfernt sind als der Mittelwert ihrer Durchmesser
  2. Wenn ihre Koordinaten x0-x1 ihre Breite und y0-y1 ihre Höhe unterschreiten

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:

Test auf Zusammenstoß Paccie-Krake
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!

Zusammenstoß Paccie-Krake
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 :-).

Abschuss und Check Treffer:
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;
      }
    }
  }
}

5. Tastatursteuerung

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!

5.1. Events abfangen

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!

"Einhängen" der Funktion taste_auswerten() ans KEYUP-Event
function 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=".....">.

5.2. Keys auswerten

Hier haben wir wieder 3 Wege, die beschritten werden müssen.

Tastencode bestimmen: Funktion 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.

5.3. Ausführen von Richtungsänderungen, Browserprobleme

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:

BrowserTastenangelieferte Codes oben,unten,rechts,links
NN4num.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
OP6w,(xy),k,j87,(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:

Interface: Reaktion auf Tastencode: 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!