Java cooperation home
Tutorial
type and press Enter

Leveleditor

In diesem Kapitel wollen wir euch eine Möglichkeit vorstellen einen Leveleditor zu programmieren, den man in jedem beliebigen, arraybasierten Javaspiel verwenden kann. Vorstellbare Anwendungsmöglichkeiten dieses Leveleditors wären z. B. ein Breakoutklon, Boulderdash, PacMan, ein "Jump and Run" Spiel oder auch ein Nibbles - Klon wie unser Spiel Snakin. Bevor ihr weiterlest solltet ihr euch mit der Verwendung von Arrays sowie Applet Parametern vertraut machen, falls ihr es noch nicht seid.

Wozu braucht man einen Leveleditor?

Obwohl die Antwort auf diese Frage eigentlich selbsterklärend ist möchte ich kurz darauf eingehen. Einen Leveleditor zu programmieren und das Spiel danach so auszurichten, dass es mit jedem beliebigen erstellten Level funktioniert, ist wesentlich schwieriger, als ein Spiel zu programmieren, das nur in einer statischen Levelumgebung läuft. Dennoch lohnt sich dieser anfängliche Mehraufwand immer dann, wenn man sein Spiel mit vielen verschiedenen Leveln ausstatten will. Hat man sein Spiel von Anfang an auf einen Leveleditor und damit erstellte Level eingerichtet, so ist das Hinzufügen weiterer Level eine Sache von wenigen Minuten. Hat man das Spieldesign dagegen nur auf statische Level ausgerichtet, so ist es sehr schwer, vielleicht sogar unmöglich, neue Level hinzuzufügen. Diese Überlegung ist im Grunde für jede Software von Bedeutung. Oft ist ein anfänglicher Mehraufwand an Designüberlegungen am Ende "Gold wert" und erspart dem Programmierer (also euch) eine Menge Ärger wenn die Software erweitert werden soll, als eine schnelle "zusammengahackte" Lösung. So, nach dieser "Moralpredigt" wollen wir uns nun dem eigentlichen Problem widmen.

Die Grundidee

Unabhängig davon, wo und wie genau wir die Level unseres Spieles definieren, was im nächsten Abschnitt erklärt wird, müssen wir uns darüber Gedanken machen, wie das Level intern im Spiel dargestellt werden soll.
Wir werden über unser Spielfeld ein rechteckiges Raster legen und in dieses Raster unsere Spielsteine bzw. Levelelemente platzieren. Somit ist die Position jedes Elementes auf dem Spielfeld eindeutig durch das Rasterquadrat, in dem es sich befindet, bestimmt. Intern werden wir hierzu eine zweidimensionale Matrix erzeugen, deren Reihen und Spalten unser gerastertes Spielfeld representieren. Wir werden nun je nach Aussehen des Levels, das wir geschrieben haben, diese Matrix mit verschiedenen Levelelementen füllen. Immer wenn wir das Level zeichnen werden wir die gesammte Levelmatrix durchlaufen und jedes Objekt in der Matrix an jener Position im Spielfeld zeichnen, an der das Object in der Matrix auftaucht. Aufbauend auf dieser ziemlich einfachen Idee wenden wir uns nun der Fragestellung zu, wo und wie wir das Level definieren wollen, mit dem wir später unsere Matrix füllen wollen.

Wo und wie definieren wir die Level?

Da wir ja Applets programmieren haben wir grundsätzlich drei Möglichkeiten wo wir die Level unseres Spiels definieren:

  1. In einer externen Datei
  2. Im Sourcecode des Spiels
  3. In der HTML Seite, in der das Applet laufen soll mit Hilfe von Appletparametern

Die Möglichkeiten 1 und 2 sind aus verschiedenen Gründen für Applets nicht so gut geeignet wie die dritte Alternative. Das Einlesen von externen Dateien in das Applet ist nicht ganz einfach und fällt daher weg. Die Level im Sourcecode des Spiels zu definieren ist dann in Ordnung, wenn man nicht will, dass der Spieler die Level einsehen kann (z. B. wenn man jedes Level mit einem Passwort versieht oder das Ziel des Spiels darin besteht ein Level zu erkunden), ist jedoch ungeeignet um dem Spieler die Definition eigener Level zu ermöglichen. Die Grundidee des dritten Ansatzes, den wir im Folgenden verwenden werden, kann jedoch bei Bedarf ohne Probleme auf die beiden anderen Ansätze übertragen werden.

Wir werden unsere Level also mit Hilfe von Appletparametern definieren. Diese können innerhalb des Applet - Tags in einer HTML - Seite definiert werden und können vom Applet mit Hilfe der getParameter (Parametername) - Methode eingelesen werden. Appletparamter haben immer die Gestallt:

    <param name= "Name des Parameters" value="Wert des Paramters">

Unsere Level werden nun immer aus 3 Informationsparametern und 8 Levelzeilen bestehen. dabei werden die Namen der Parameter immer folgenden Aufbau haben: "Level" + "Levelnummer" + "_" + "Identifikator". Der Identifikator kann dabei die Werte "Author", "Name", "Comment" oder "Line" + "Zeilennummer" annehmen. Die value - Strings können im Falle der Informationsparameter beliebige Strings sein, die Levelzeilen hingegen werden immer aus 10 Buchstaben bestehen die die einzelnen Steine des Levels in verschiedenen Farben bestimmen. Diese Farben sind r = rot, g = grün, b = blau und y = gelb sowie ein Platzhalter ":" der Stellen definiert, an denen kein Stein plaziert werden soll. Die einzige Stelle an der sich die die Namen der Levelparameter verschiedener Level unterscheiden ist also die Levelnummer. Daher können wir leicht in einer while oder for - Schleife alle Level von 1 bis zu der in "Levels_in_total" definierten Zahl nacheinander einlesen (siehe readLevel - Methode unten). Im Folgenden seht ihr die Struktur eines Levels, das unser Leveleditor einlesen kann.

    // Beginn des Applettags, mit normalen Applet Informationen
    <applet code = Main width=300 height=400>

    // Diese Zeile gibt an, wieviele Level insgesammt definiert sind
    <param name="Levels_in_total" value="1">

    // Diese Zeilen speichern allgemeine Informationen über unser Level
    <param name="Level1_Author" value="FBI">
    <param name="Level1_Name" value="Test Level 1">
    <param name="Level1_Comment" value="My first try">

    // Das eigentliche Level
    <param name="Level1_Line0" value="rrrrrrrrrr">
    <param name="Level1_Line1" value="bbbbbggggg">
    <param name="Level1_Line2" value="r::rrrr::r">
    <param name="Level1_Line3" value="yyyyybbbbb">
    <param name="Level1_Line4" value="rrr::::rrr">
    <param name="Level1_Line5" value="gggggyyyyy">
    <param name="Level1_Line6" value="r::rrrr::r">
    <param name="Level1_Line7" value="bgybgrybgy">

    // Ende des Applet Tags
    </applet>

Das Klassendesign des Leveleditors

Um nun ein, auf die oben dargestellte Weise definiertes, Level in unser Spiel einzulesen und dort verwenden zu können werden wir für unseren Leveleditor nun folgende Klassen benötigen:

  1. LevelReader:

    Diese Klasse wird die in dem Parameter "Levels_in_total" angegebene Zahl von Leveln mit Hilfe der getParameter(Parametername) - Methode einlesen und für jedes gelesene Level eine Instanz der Klasse Level (siehe unten) erzeugen sowie alle erzeugten Level in einem Levelarray speichern. Anschließend gibt die Methode readLevels(), die das Einlesen bewerkstelligt, das erzeugte Levelarray an die aufrufende Klasse zurück.

  2. Level:

    Diese Klasse speichert zum Einen alle Informationen über ein Level, also Name des Authors, Name des Levels und die Beschreibung. Zum Anderen verfügt jedes Level über ein zweidimensionales Array (stone map), das die Positionen der Steine in einem Level speichert bzw. Objekte der Klasse Stone (siehe unten), die ihre jeweilige Position und Farbe "kennen". Außerdem hat jedes Level noch eine eigene paint - Methode, die das Level zeichnet.

  3. Stone:

    Diese Klasse speichert Farbe und Position eines Steines in einer Stonemap. Die Position des Steines wird im Konstruktor aufgrund der Position des Steines in der Stonemap des Levels berechnet. Ein Stein verfügt zudem über eine eigene paint(Graphics g) - Methode, die den Stein an der richtigen Position im Level und in der richtigen Farbe zeichnet.

  4. Main:

    Diese Klasse speichert das Levelarray und zeichnet das Level, das im Moment aus dem Levelarray ausgewählt ist. Außerdem behandelt sie die Benutzereingaben.

  5. C_LevelEditor:

    Diese Klasse beinnhaltet alle im Leveleditor vorkommenden Konstanten, wie z. B. die Anzahl der Zeilen aus der ein Level besteht, den Abstand der Steine zueinander sowie ihre Größe, die Namen der Identifikatoren... . So lässt sich der Leveleditor leicht auf mehrere Levelzeilen, veränderte Größe der Rasterquadrate, andere Identifikatornamen... erweitern.

Der Sourcecode der wichtigsten Methoden

Wie immer werde ich nicht auf jede einzelne Methode des Leveleditors eingehen. Ich werde im Folgenden nur die wichtigsten Methoden und Klassen besprechen. Dazu gehören LevelReader und seine Methode readLevels(), sowie die Klasse Level. Auf die Klassen Main, Stone und C_LevelEditor werde ich nicht näher eingehen, was heißt, dass ihr euch wie immer den Sourcecode runterladen und euch die anderen Methoden und Klassen selbst ansehen solltet.

LevelReader

Zunächst einmal müssen wir alle in der HTML - Seite definierten Level in unser Spiel einlesen und als Levelobjekte in einem Array aus Levelelementen speichern. Dies geschieht in der Klasse LevelReader mit Hilfe der Methode readLevel(). Die Methode an sich ist relativ selbsterklärend, einzige Besonderheit ist, dass wir dem Levelreader eine Refferenz auf die Elternklasse des Applets (Parent) übergeben müssen, damit er mit der getParameter - Methode auf die Appletparameter in der HTML - Seite zugreifen kann. So, hier kommt die Klasse LevelReader:


    import java.util.*;
    import java.applet.*;
    import java.awt.*;


    public class LevelReader
    {
      // Variablen
      private int levels_in_total;

      // Array, speichert alle erzeugten Level
      private Level [] level_array;

      // Appletrefferenz
      private Component parent;

      public LevelReader (Component parent)
      {
        // Initialisierung der Appletrefferenz
        this.parent = parent;

        // Gesamtzahl von Leveln
        levels_in_total = Integer.parseInt (((Applet)parent).getParameter (C_LevelEditor.total_levels));

        // Initialisierung des Arrays
        level_array = new Level [levels_in_total];
      }

      /* Diese Methode liest alle Level in der HTML Seite ein, erzeugt ein
      dementsprechendes Level und speichert es in dem Levelarray. */
      public Level [] readLevels ()
      {
        for (int i = 1; i <= levels_in_total; i++)
        {
          // Neues Level generieren
          Level level = new Level ();

          // Alle wichtigen Informationen über das Level holen und setzen
          level.setAuthor (((Applet)parent).getParameter ("Level" + i + "_" + C_LevelEditor.author));
          level.setName (((Applet)parent).getParameter ("Level" + i + "_" + C_LevelEditor.name));
          level.setComment (((Applet)parent).getParameter ("Level" + i + "_" + C_LevelEditor.comment));

          // Levelzeilen einlesen und im Level speichern
          for (int j = 0; j < C_LevelEditor.number_of_lines; j++)
          {
            level.setLine (((Applet)parent).getParameter ("Level" + i + "_Line" + j), j);
          }

          // Level speichern
          level_array [i-1] = level;
        }
        return level_array;
      }
    }

Die Klasse Level

Nachdem wir nun alle Level eingelesen haben, wollen wir sie in unserem Spiel darstellen. Dazu haben wir die Klasse Level. Wie schon oben erwähnt ist die wichtigste Instanzvariable der Klasse Level das zweidimensionale Array stone_map. In diesem Array werden die Stoneobjecte gespeichert, die im jeweils zur Zeile und Spalte der Matrix äquivalenten Rasterquadrat des Levels gezeichnet werden sollen. Die Klasse Level verfügt zunächst einmal über diverse Methoden um die Informationen aus den Levelparametern zu speichern. Interessanter ist da schon die Methode setLine(...). In ihr wird die Information aus einem bestimmten Levelparameter, der ja eine Zeile der stone_map Matrix darstellt, in Stoneobjecte übersetzt, die Details seht ihr dann im Sourcecode. Zu guter Letzt hat die Klasse Level noch die Methode paintLevel(...), die das gesammte Array des Levels durchläuft und die Stoneobjecte zeichnet.

    import java.util.*;
    import java.awt.*;

    public class Level
    {
      // Variablen
      private String author;
      private String name;
      private String comment;

      // Levelmatrix, speichert die Steinobjecte
      private Stone [] [] stone_map;
      public Level ()
      {
        // Initialisierung der Matrix, alle Felder werden mit Null initialisiert
        stone_map = new Stone [C_LevelEditor.number_of_lines]
        [C_LevelEditor.number_of_cols];
      }

      // Methode übersetzt jeweils eine Zeile der Leveldefinition in Levelelemente
      public void setLine (String line, int line_index)
      {
        char [] entrys = line.toCharArray();

        // Durchlaufen jedes Buchstabens und übersetzen in Stoneobject oder Null
        for (int i = 0; i < C_LevelEditor.number_of_cols; i++)
        {
          Stone stone = null;

          // Bei Buchstaben 'r' erzeugen eines roten Steines
          if (entrys[i] == 'r')
          {
            stone = new Stone (line_index, i, Color.red);
          }

          // Erzeugung der anderen Levelelemente analog
          ...

          // Ist kein bekannter Buchstabe vorhanden, so keinen Stein erzeugen
          else
          {
            // do nothing
          }

          // Stein im Array speichern
          if (stone == null)
          {
            // do nothing
          }
          else
          {
            stone_map [line_index] [i] = stone;
          }
        }
      }

      // set Methoden für die Levelinformationen
      ...

      // Methode zeichnet das Level
      public void paintLevel (Graphics g)
      {
        // Gesammtes Stonearray durchlaufen
        for (int i = 0; i < stone_map.length; i++)
        {
          for (int j = 0; j < stone_map[i].length; j++)
          {
            Stone stone = stone_map [i][j];

            // Entweder Stein zeichnen oder nichts zeichnen
            if (stone == null)
            {
              // draw nothing
            }
            else
            {
              stone.drawStone(g);
            }
          }
        }

        // zeichnen der Informationen
        g.setColor (Color.yellow);
        g.drawString (comment, 50, 250);
        g.drawString (name, 50, 270);
        g.drawString (author, 50, 290);
      }
    }

Zusammenfassung

Wir haben also nun gelernt, wie man Level in der HTML - Seite definieren könnte, wie man die so definierten Level in das Spiel einließt und dort zum Erzeugen bzw. Zeichnen des Levels verwendet. Wie immer gibt es viele verschiedene Möglichkeiten, dies zu bewerkstelligen und nicht nur die eine, die wir hier vorgestellt haben. Obwohl wir euch mit diesem Leveleditor möglicherweise einiges an Arbeit erspart haben (da ihr das Einlesen und Abspeichern fast eins zu eins übernehmen könnt), steht euch die größte Aufgabe noch bevor. Ihr müsst euer Spiel darauf einrichten, mit jedem beliebigen Level zu funktionieren, was gar nicht so einfach ist und natürlich ist dieser Leveleditor noch in keinster Weise auf euer Spiel abgestimmt, was Zeilen- und Spaltenzahlt, die verwendeten Levelelemente... angeht. Also, viel Spaß beim Erweitern des Leveleditors und wenn ihr ein Spiel unter Verwendung dieses Editors geschrieben habt, würde ich mich freuen, wenn ihr mir es zusendet. Wie immer am Ende eines Kapitels könnt ihr euch den Sourcecode runterladen und euch das fertige Applet ansehen.

SourceCode download
Applet ansehen

Nächstes Kapitel

Scrolling
Fabian Birzele, 2001-2004.
web-design: Vadim Murzagalin, 2004.