Making of Tetrix J2ME für KRZR K1 (sprich: KRatZeR)

Voraussetzungen:

Inhalt

  1. Reicht das Werkzeug für das Projekt?
  2. Klassenhierarchie (Bauplan)
  3. Erstellen eines Projektes mit dem WTK
  4. Erstellen der Grafiken
  5. Erstellen der Levelfiles
  6. Klasse main, der Big Boss
  7. MIDP-Hi-Level UI: Form
  8. MIDP-Low-Level UI: Canvas
  9. Berechnungen fürs Spiel
  10. Tetrix-Steine
  11. Highscore-Listen: rms als permanenter Speicher
  12. Test im WTK, pre-verifier,Erstellen des Jars,OTA,Installer,Icons
  13. Test auf Handy,usb-Anschluss,kjava,Installer(Abweichungen),Icons(Abweichungen)

1. Was kann die J2ME-Bibliothek, wa das Handy auch kann?

Das KRZR behauptet, MIDP2.0-konform zu sein.

Also kann man nach Herzenslust in Suns Doku zum J2ME nach brauchbaren Klassen suchen. Diese findet man unter sun-j2me-bin-2.2/docs/api/midp/index.html. Das Icon dieser Doku sollte immer parat liegen ;).
Dort gibt es u.a. eine Canvas-Klasse, mit der man sich grafisch austoben kann, und Klassen zum Laden der Bilder und zum Generieren aus anderen Bildern. Na, das ist doch schon, was wir brauchen!
Des weiteren sind Funktionstasten definiert, und man hat ein Menu zur Verfügung. - Na, viel mehr braucht das Javascript-Tetrix auch nicht.

Denn man tau!


zur Inhaltsangabe

2. Klassenhierarchie (Bauplan)

Objekthierarchie

Main ist der Boss, er erzeugt sich die Objekte, die auf den Bildschirm schreiben (canvas, canvas2, form).

canvas erzeugt sich das Objekt my_game, my_game bedient sich mit dem Objekt ldata aus der Klasse leveldata, um seine Belegungstabelle zu initialisieren, sowie aus der Klasse chip aktchip, das Objekt zum Fallenlassen und Rumschubsen, und nextchip, seinen Nachfolger. Als letztes braucht es noch die ein Objekt (my_file) der Klasse loadsave, eine Wrapper zum Laden/speichern von RecordStore-Daten, um Zwischenstände zu speichern.

Big Boss kommandiert auch den Highscore-Canvas (Objekt canvas2), der wiederum erzeugt ein Objekt der Klasse highscore, welches den Underdog fh der Klasse loadsave bemüht.


Warnung: zeitkritische Objekt-Methoden sollten nicht unterhalb von my_game angesiedelt werden, das Handy schneckt sonst ganz fürchterlich!


zur Inhaltsangabe

3. Erstellen des Projektes mit dem WTK

Wir starten aus Suns Developer Kit das WTK sun-j2me-bin-2.2/bin/ktoolbar, wählen "New Project", geben den Projektnamen tetrix ein und als Midlet-Namen tetrix.main. Der Name tetrix muss davorgestellt werden, da es sich um den Package-Namen handelt, ohne das das WTK2.2 die Hauptklasse main nicht findet (das mit den Klassen und Packages kommt später).

Wichtig ist, dass das WTK uns die passenden Verzeichnisse anlegt, in der man deine Ergüsse so unterbringen kann, dass das Ganze ein anständiges JAR ergibt.

Zudem kann man nach Erstellen des Projektes in den Preferences die Handy-Spezifikationen einstellen. Bei Verwendung von Klassen, die dem Handy fehlen, wirft der Compiler dann eine Fehlermeldung.

Das Kratzer kann JTWI, d.h. MIDP2.0 unf CLDC 1.0.
Allgemein gilt, so restriktiv wie möglich programmieren und nur explizit angegebene APIs benutzen, also bleibt es bei CLDC1.0, der Rest wird nicht angeklickt.

Das WTK legt die Pfade unterhalb von apps an. Nun liefert Sun schon jede Menge Applikationen mit, es kann ratsam sein, sich besser ein neues Arbeitsverzeichnis einzurichten. Dazu trägt man in der Datei sun-j2me-bin-2.2/wtklib/(Linux)/ktools.properties ein:
kvem.apps.dir = ujagames (oder was auch immer)

tetrix findet man innerhalb dieses Applikationsverzeichnisses wieder, darunter Verzeichnisse, von denen zunächst res und src für uns interessant sind.


zur Inhaltsangabe

4. Erstellen der Grafiken

Am besten erstellt man sich hier einen Plan, wie der Bildschirminhalt auszusehen hat.

Das Kratzer besitzt ein Display mit 176x220 Pixeln. Nur leider kann man nicht alle verwenden, laut MIDP2 müssen Batterieanzeige und Empfang sichtbar bleiben. Ein weiterer Teil ist für die "Softkeys" belegt.
Als brauchbar hat sich ein Bereich von 176x176 erwiesen, und derzeit wollen wir ohne Gesamtschirmflag auskommen.

Das Original-ujaswelt-Tetrix-Feeling (Turbo Pascal) soll erhalten bleiben, das bedeutet: das Feld ist 10 Steinchen breit und 20 Steinchen hoch, dazu kommt ein schmaler Rahmen.
176/20 = 8.8 px, das bedeutet 8px Kantenlänge, und es bleiben 16 px für den Rand. Wir lassen das Feld bei y=0 anfangen, das erleichtert obendrein die Berechnung. 12px (die Textgröße des KRZR-Small-Font) sollen freigelassen werden für eine eigene Statusanzeige, bleibt für den Rand 4px.

Steinchen auf Dunkelblau Der Handy-Bildschirm ist oft nicht sehr kontrastreich, also arbeiten wir so knallbunt wie möglich: auf einen dunkelblauen Hintergrund und ein eher grau erscheinenden Rahmen mit hellgelber Schrift erscheinen knallbunte Steine (Farb-Sättigung 100%), deren Rand halb so hell, und im Stein selbst ein Lichtpunkt. Gespeichert werden soll das Ganze als PNG.

Dank MIDP2 brauchen wir nicht -zig Einzel-PNGs, sondern können alle benötigten Grafiken in 1 Bild verpacken, das spart 10-20% JAR-Platz (gecheckt an tetrix.jar, 29k, und tetrix_m1.jar, >33k).

Weiteren Platz kann man sparen, indem das PNG nicht einfach roh, sondern als 255-Farb-Grafik abgespeichert wird. Wegen des Handy-Displays reichen auch 127 Farben (gecheckt mit Gimp 2.3, 2.4), z.B. sieht man den Unterschied Lindgrün-Sattgrün im Emulator deutlich, auf dem Kratzer kaum noch, auch das Hellgelb erscheint als schnödes Weiss, und über die Darstellung des so sorgfältig gerenderten Schriftzuges Tetrix decken wir mal das Mäntelchen des Schweigens! Original-Schriftzug was das KRZR davon machte

Warum ist es wichtig, Platz zu sparen?

  1. Meine Geduld ist zu Ende, wenn das Kratzer so um die 90K geladen hat, ein Haupt-Ärgernis der mitgelieferten Spiele.
  2. Es ist das Handy meines Mannes, und der speichert lieber Fotos und Filmchen als Spiele auf seinem Schätzchen.
  3. Die Handys haben nicht die Downloadrate eines DSL-6000-Anschlusses,
  4. Das Java-System des Handys selbst bricht zu fette Downloads ab, und da hat sich 100k als Grenzwert herausgebildet (kann geändert werden).

Die Grafikdatei ist fertig, aber wohin jetzt damit?

Für Programmzubehör wie Texte und Grafiken ist das Verzeichnis /res (ressources) gedacht. Hier erstellen wir ein Verzeichnis images für das/die Bild/er und ein Verzeichnis data für die Levelcodes sowie weitere Daten.

Im Endergebnis liegt unser Bild von /res aus gesehen in /images/tiles8x8_glas.png (Grafik 8x8), die Titelgrafik packen wir auch dazu.


zur Inhaltsangabe

5. Die Levelfiles

Am Anfang steht die Definition:

Level 0 besteht somit aus 20 Reihen zu 10 Nullen, bekommt den Namen tetrix.0 und wandert nach /res/data.
In Level 1 bauen wir ein paar Stör-Steine ein, nennen das Ganze tetrix.1 und schieben ebenfalls nach /res/data
(kann beliebig fortgesetzt werden)

Levels im Jar:

Level 0:
0000000000 0000000000 0000000000 0000000000 0000000000 0000000000 0000000000 0000000000 0000000000 0000000000 0000000000 0000000000 0000000000 0000000000 0000000000 0000000000 0000000000 0000000000 0000000000 0000000000
Level 1:
0000000000 0000000000 0000000000 0000000000 0000000000 0000000000 0000000000 0000000000 0000000000 0000000000 0000000000 0000000000 0000000000 0000000000 0000000000 0808008080 0000000000 0000000000 0000000000 0000000000
Level 2:
0000000000 0000000000 0000000000 0000000000 0000000000 0000000000 0000000000 0000000000 8000000008 8800000088 8880000888 8888008888 8000000008 8800000088 8880000888 8888008888 8000000008 8800000088 8880000888 8888008888

Wie kommen die Levels ins Programm?

Der Zugriff auf Dateien ist für uns Normal Sterbliche (die, die kein Geld haben, jeden Furz zu signieren) verboten. Wir dürfen nur auf Inhalte des Jars und das nur lesenderweise zugreifen. Aber das reicht vorerst. MIDP erlaubt uns dazu die Verwendung einen Streams.

Wir erstellen uns eine Klasse leveldata, die das Feld je nach Level fix und fertig zurückliefert.

Die Klasse gehört zu unserem package tetrix, das müssen wir im Kopf des Source-Files angeben. Sie benötigt dringend die io-Bibliothek, das muss man ihr auch mitteilen.

Leveldata-Hülle:

// uja 7/2007 tetrix fuer Motorola KRZR 1
// --------------------------------------------------------------------------------------------------
package tetrix;
import java.io.*;

class leveldata // Leveldaten lesen aus jar-file:
{ 
  public int[][] get_level(int level,int xmax,int ymax)
  { 
    int b[][]=new int[xmax][ymax];
    b[0][0]=-1;

    return b;
  }
}

get_level(lnr,xmax,ymax) holt also das Level Nr. lnr, was ein Feld xmax x ymax versorgen soll. xmax und ymax werden von der Klasse game bestimmt, (canvas und) leveldata haben sich daran zu halten. Wird das Level nicht gefunden, enthält das erste Element -1, was in der Leveldefinition nicht vorkommt.

Zum Datenlesen aud dem jar sind Streams erlaubt. Aber wie kommen wir an den Stream ran?

Alle Klassen leiten sich von Object ab, haben also dessen Methoden. Eine davon ist getClass(), sie liefert die "echte" Klasse des Objektes. Man kann also getClass dazu benutzen, herauszufinden, wes Geistes Kind das Objekt ist, oder sich einen Handle drauf besorgen.

Die Klasse Class ist von Object abgeleitet, siet hat keinen zugänglichen Konstruktor. Man kann aber, wie oben erwähnt, mit getClass auf sie zugreifen. Und Class hat tatsächlich eine Methode, die einen InputStream liefert: getRessouceAsStream(String), das eine Datei mit Pfad als Parameter akzeptiert.

Das Handbuch zum WTK 2.2 sagt, dass Ressourcen-Pfade des Jars immer in /res beginnen. Also besorgen wir uns ein Handle auf die Levels: InputStream is = getClass().getResourceAsStream("/data/tetrix."+level); (vgl. MOTOKRZR_K1_Developer_Guide.pdf, Kap.13, Seite 87)

Nun müssen wir Vorkehrungen treffen, was passieren soll, wenn die Date nicht gefunden wird. Es reicht nicht, nichts tun zu brauchen, man muss es dem Programm auch sagen, wenn der seine Exception wirft: "Kann ich nicht!". (Fast) alle Java-Bibliotheken werfen mit Exceptions um sich, wenn sie an der Arbeit gehindert werden. Dazu dient die try- catch- Konstruktion, die mittlerweile auch Einzug in Javascript gehalten hat. try leitet einen Block mit Methoden ein, die danebengehen können. Wird eine Exception geworfen, geht es mit dem catch-Block weiter.

leveldata, verfeinert um Ressource-Handle:

// uja 7/2007 tetrix fuer Motorola KRZR 1
// --------------------------------------------------------------------------------------------------
package tetrix;
import java.io.*;

class leveldata // Leveldaten lesen aus jar-file:
{ 
  public int[][] get_level(int level,int xmax,int ymax)
  { 
    int b[][]=new int[xmax][ymax]; // das zurückgelieferte Feld
    b[0][0]=-1;                    // Indikator für game, falls Level nicht gefunden wird
    InputStream is=null;           // Indikator, falls Jar-Ressource nicht gefunden wird
    try
    { is = getClass().getResourceAsStream("/data/tetrix."+level);

      // hier fehlt noch der Lese/Konvertiervorgang

      is.close();
    }
    catch (java.io.IOException ex)
    {
      // nochmal abfangen - bequemer zum Debuggen ;)
      // Vom Prog her könnte dies auch leer bleiben
      System.out.println("Level "+level+" kann nicht gelesen werden!");
    }
    return b;
  }
}

Wir haben ein Handle auf das levelfile im Jar samt Fehlerbehandliung, da fehlt nur noch das Einlesen und Konvertieren.
Zur Auswahl bietet der InputStream so was wie BlockLesen (das nehmen wir beim Hilfe-Text) und byteweises Lesen. Da die Daten auch Dröpje for Dröpje in ein Integer-Feld konvertiert werden sollen, nehmen wir das byteweise Lesen int inhalt=is.read(), wobei Zeichen, die nicht zum Levelcode gehören, unbesehen unter den Tisch fallen.

Und schon stoßen wir auf eine der vielen Ungereimheiten der Sprache Java, die einen Tester dazu veranlasste, mit "Java ist Flickerkram" zu unterschreiben, und diesmal bezog sich das nicht auf einen flackernden Bilschirm eine Java-Applikation!

InputStream.read(): Gelesen wird ein Byte, zurückgeliefert ein Integer!

Weitere Inkonsistenzen gibt es z.B. bei trunc, round, und floor.

Obiges ist nur entschuldbar, um irgendwie an die -1 zu kommen, wenn kein Byte mehr im File ist. Die Handy Javas sind sich da nicht so einig, so liest z.B. ein Nokia auch nach Fileende munter weiter. Hier wird mit counted Arrays gearbeitet (Turbo Pascal-Ausdruck), eine -1 wird da niemals zurückgeliefert.

Also sehen wir zu, dass das Lesen spätestens abbricht, wenn alle Feldelemente ihren Wert haben. Das ist der Fall, wenn der Zeilencounter auf ymax landet.

Was passiert hier im schlimmsten Fall?
Es wird so lange gelesen bis alle Felder plausible Werte haben oder eine Exception geworfen wird. Dann sieht es im unteren Teil des Spielfeldes eventuell kunterbunt aus, aber damit kann man spielen. Kaputte Levels sind ja vielleicht für den Zocker ganz interessant.

leveldata in voller Schönheit:

// uja 7/2007 tetrix fuer Motorola KRZR 1
// --------------------------------------------------------------------------------------------------
package tetrix;
import java.io.*;

class leveldata // Leveldaten lesen aus jar-file:
{ 
  public int[][] get_level(int level,int xmax,int ymax)
  { 
    String code="0123456789abcdef";   // unser Levelcode, wie zu Beginn definiert
    int i=0,j=0,k;                    // counter Zeile, Spalte, gelesenes byte/integer
    int[][] b=new int[xmax][ymax];    // das Levelfeld
    b[0][0]=-1;                       // Wert bei Misserfolg
    InputStream is=null;
    try 
    { 
      is = getClass().getResourceAsStream("/data/tetrix."+level);
      while (((k=is.read())!=-1) && (j<ymax)) // nicht übers Ende hinauslesen
      { 
        if (code.indexOf(k)>=0)     // falls k im Levelcode vorkommt
        {
          b[i][j]=code.indexOf(k);            // belege Feld mit entsprechendem Wert
          i=(i+1)%xmax;                       // Spaltencounter anpassen
          if (i==0) j++;                      // wenn Reihe voll, Zeilencounter anpassen
        }
      }
      is.close(); 
    }
    catch  (java.io.IOException ex)
    {
      System.out.println("Level "+level+" not found");
    }
    return b;
  }
}

Und wohin damit?
Das WTK hat da für uns schon was vorbereitet: Quelltexte gehören in (apps)/tetrix/src, also speichert man dies in (apps)/tetrix/src/leveldata.java.

Wer will kann schon mal den Compiler des WTK (Build) drüberlaufen lassen, um Dreckfehler zu korrigieren. Dessen Compilat findet man unter (apps)/tetrix/classes/tetrix.



zur Inhaltsangabe

6. Klasse main, der Big Boss

Die Applets der Handys heissen MIDlets, abgeleitet von MIDP.(Mobile Information Device Profile). Wie Javascript ist das MIDP ein Sandbox-System, also recht restriktiv und gesichert vor Amokläufen (Habe noch mehr Texte dazu, Dieter!). Das bedeutet, dem Handy wird kein Schaden durch ein MIDlet durchgeführt, und wenn doch, hat der Hersteller Schxxsse gebaut.

Ein Midlet ann sich in 4 Zuständen befinden. Nach dem Durchlauf des Konstruktors befindet es sich zunächst in dem Zustand loaded und geht nach Initialisierung aller Daten/ Durchlauf durch den Konstruktor sofort in paused über. Dafür sorgt das AMS (Application Management Software).
startApp läuft an, bringt es in Status Active, springt dann bei Bedarf (Anruf, softwaremäßig mit notifyPaused() oder Exception) in pauseApp (Status paused), wird pauseApp beendet (aufgehängt), wird startApp wieder aufgerufen. Das ganze MIDlet beendet sich mit destroyApp (macht den Weg frei für die Langoliers, äh, die Garbage Collection). Das Programm kann in destroyApp noch einen letzten Wunsch angeben, bevor es gefressen wird.


Das heisst: im Konstruktor belegt man am besten alle möglichen Objekte mit Daten, kurz und gut: da gehört alles rein, was nur ein einziges Mal ablaufen soll. Dabei sollte man allerdings Teile, die nicht unbedingt gebraucht werden wie die Info und die Hilfedatei sowie das rms-System davon ausnehmen. Sparen ist angesagt!
Diese Vorgehensweise ist zumindest in Deutschland und der Schweiz und vor allem von mir :) anerkannt.

In startApp wird zweckmäßigerweise auf den bevorzugten Bildschirm umgeschaltet sowie eine Animation oder eine Stoppuhr gestartet.

In einem Sun-Artikel vom Oktober 2002 wird behauptet, dass des den load-Zustand bei Midlets nicht gibt, das Midlet direkt in paused übergeht und sich beim ersten Aufruf von startApp initialisiert. Schafft es das nicht, wirft es das Handtuch, äh, eine MidletStateException und ergibt sich den Langoliers (Status destroyed).

Sun hat hier die Initialisierung ganz einfach an die AMS (Application Management Software) abgeschoben!

In dem gleichen Artikel wird mit nicht nachvollziehbaren Gründen davor gewarnt, Daten im Konstruktor zu initialisieren. Die Gründe dürften eher sein, dass die AMS von ihrem Glück noch nichts wusste! (2002)

Eigene Versuche zeigten:

In startApp sollte nichts, aber auch nichts, initialisiert oder generiert werden! Finger weg von new!

... oder man hat ein prächtiges Speicher-Leak oder doppelt und dreifach laufende Zeit-Threads, was im Emulator noch ganz putzig aussieht, aber für ziemliche Hektik auf dem armen Handy sorgt!

Ein anderer Effekt war weitaus weniger dramatisch, aber für den Anwender doch recht ärgerlich. Gemäß der Sun-Empfehlung hatte ich in der 1.Version des Tetrix das canvas.my_game.neu(0,0) in der startApp eingebaut. Das hatte den Effekt, dass nach einer Unterbrechung des Spiels Level und Punktestand wieder auf 0 waren, und der arme Zocker sich umsonst abgezappelt hatte, falls er nicht vorher den Spielstand in das RMS (s.unten) geschoben hatte. In der jetzigen Version ist dieser Fehler korrigiert.

Ich selbst löse es mittlerweile so, dass ich möglichst viel in der Variablendeklaration am Kopf der Klasse generiere, wenn möglich, mit Anfangsdaten belege, und im Konstruktor alles, was ich nicht "erwischen konnte", mit Werten belege. Nicht initialisierte Variable sind mir und auch dem Compiler ein Greuel! (Soll die AMS doch schwitzen!)

Mit dieser Vorgehensweise ist mir noch kein Programm vor die Wand gelaufen, weder im Emulator noch auf dem KRZR K1.

Sun löst dieses Dilemma so, dass der Konstruktor leer bleibt, dafür gibt es zusätzlich eine selbstgeschriebene Methode init(), die von startApp manuell angestoßen wird. Dann wird ein flag gesetzt, damit bei weiteren Umschaltungen auf Active der Init-Durchlauf übersprungen wird. (Artikel Oktober 2002). Also ein typischer Workaround für einen Bug!

Folgerichtig wird in der Doku zur WTK 2.2 bei der Beschreibung der Bibliothek Display darauf hingewiesen, dass die Variableninitialisierung im Konstruktor des Midlets zu erfolgen hat (Kapitel über startApp).

pauseApp kickt das Midlet in den Zustand Paused, hier kann man in den Spielen die Animation auslaufen lassen und eine Stoppuhr anhalten. Es wird nur nach startApp aufgerufen.

Tja, und für destroyApp bleiben die Aufräumarbeiten. Diese Methode ist meist leer, weil die Garbage Collection sich eigentlich um so was kümmert. Leider musste ich feststellen, dass die GC des Handy-Emulators des WTK seit mehreren Versionen bis zu 2.2 das nur halbherzig tut und man bei Threads in destroyApp nachhelfen muss. - Und wer weiss, ob nicht einige Handys dieselbe Macke haben!

main ist vom Stamme, äh, der Klasse MIDlet, die sich in der Bibiothek javax.microedition.midlet.MIDlet befindet. MIDlet wirft mit einer MIDletStateChangeException um sich, wenn es den Zustand nicht wechseln kann, die findet man in der Bibliothek javax.microedition.midlet.MIDletStateChangeException, zu guter Letzt sollte man noch die Bibliotheken für das MIDP-Display und sein UserInterface laden javax.microedition.lcdui.*. (Quelle: Sun)

Folgerichtig sieht das Grundgerüst so aus:

main - Klasse Grundgerüst

// Tetrix fuer KRZR, uja0707
package tetrix;

import javax.microedition.lcdui.*;
import javax.microedition.midlet.MIDlet;
import javax.microedition.midlet.MIDletStateChangeException;
import java.io.*;

public class main extends MIDlet implements CommandListener
{
  public main()
  {
  }

  public void startApp() throws MIDletStateChangeException
  { 
  }

  public void pauseApp()
  { 
  }

  public void destroyApp(boolean unc) throws MIDletStateChangeException
  { 
  }

}

MIDP-Commands:

Was soll jetzt das mit dem implements CommandListener?

MIDP (die Spezifikation) stellt ein UserInterface (UI) bereit, mit dem man Menus mit Kommandos zusammenbauen kann, ähnlich der Menus, die man auf der rechten Seite der gamecraft-Spiele sieht. Und genau das wollen wir haben! (Quelle: Wikipedia)

lcdui stellt uns die ganzen Schönheiten bereit.

Einzubauen ins MIDlet sind also ein Menu (rechter Teil der Javascript-Spiele), ein Screen zum Spielen (linker Teil der Javascript-Spiele), ein Screen für den Highscore-Kram (in Javascript das Popup), ein Screen für Hilfetexte (in Javascript noch ein Popup).

HTML-Menuteil:

Und die gleichen Kommandos sollen bei den Handy-Spielen verfügbar sein. Männe wollte für seine Freunde eine englische Version, daher der englische Text bei der Definition der Commands.

Java-Äquivalent:

 Command neu =new Command("New Game",Command.SCREEN,1); 
 Command help=new Command("How To",Command.SCREEN,2);
 Command cont=new Command("Continue Game",Command.SCREEN,3);
 Command lade=new Command("Load Game",Command.SCREEN,4);
 Command save=new Command("Save Game",Command.SCREEN,5);
 Command hsc =new Command("Show Hiscore",Command.SCREEN,6);
 Command hireset=new Command("Clear Highscore",Command.SCREEN,7);
 Command exit=new Command("Quit",Command.EXIT,8); 
 Command info=new Command("Info",Command.SCREEN,9);

Das Highscore-Resetten habe ich in den Javascript-Spielen für euch erledigt, hier müsst ihr selbst Hand anlegen,, daher gibt es noch das Zusatzkommando hireset.

Obiges Beispiel führt noch nicht die Kommandos aus, sondern definiert sie nur in ihrem Aussehen. So erscheint beim Command neu String "New Game" (das Label des Commands), das Kommando selbst gehört zumTyp SCREEN. Quit gehört zum Typ EXIT.
Typenzugehörigkeit bewirkt, dass, 1. das Kommando an der vom Handy vorgesehenen Stelle erscheint (Soft-Button-Beschriftung, Popup oder sonstiges), 2. falls der Handy-Hersteller seine eigene Beschriftung dafür parat hat, das Label unterdrückt wird. (Quelle:Sun)

Man kann, um dem System noch mehr Freiheiten zu geben, ein kurzes und ein langes Label geben, das System sucht sich dann das aus, was ihm gerade passt. Näheres dazu in der Sun Doku (s.o., Icon auf der Arbeitsoberfläche).

Der 2.Parameter gibt die Priorität des Kommandos an: 1 steht für die höchste, 99 für die niedrigste.
Das scheint erst mal sinnfrei, das es auch für die größten Fingerakrobaten unmöglich sein dürfte, 2 Kommandos auf einmal auszulösen.
Sun beschreibt aber auch, dass die Kommandos je nach Wichtigkeit in das Menu eingebaut werden, also 1 ganz oben, 99 unten. Das ist wichtig zu wissen, weil das KRZR K1 nur 5 Screen-Kommandos darstellen kann, ohne dass man scrollen muss., wohingegen der Emulator ein schnuckeliges, komplettes Menu anzeigt, das einen dazu verführt, dieses richtig schön auszubauen. Auf dem Handy folgt dann der Praxisschock! ("passt nicht!") - Zumindest stehen die wichtigsten Kommandos dann ganz oben.

command(String label,Command.typ,int Priorität) erzeugt so was wie ein Button in HTML-Documenten, es bestimmt das Aussehen, aber es ist nichts dahinter (kein Script irgendeiner Art).
Labels von Typen werden ignoriert, wenn der Hersteller des Handy eigene Labels für diesen Typ parat hält.
Gibt man ein kurzes und ein langes Label an, sucht sich das System das Passende raus.

Und wo gehört sowa nun hin? Zu den Definitionen.

MIDlet mit Command-Definitionen:

package tetrix;

import javax.microedition.lcdui.*;
import javax.microedition.midlet.MIDlet;
import javax.microedition.midlet.MIDletStateChangeException;
import java.io.*;

public class main extends MIDlet implements CommandListener
{
   Command neu=new Command("New Game", Command.SCREEN,1);
   Command help=new Command("How To", Command.SCREEN,2);
   Command cont=new Command("Continue", Command.SCREEN,3);
   Command lade=new Command("Load Game", Command.SCREEN,4);
   Command save=new Command("Save Game", Command.SCREEN,5);
   Command hsc=new Command("Show Hiscore", Command.SCREEN,6);
   Command hireset=new Command("Clear Highscore",Command.SCREEN,7);
   Command exit=new Command("Quit", Command.EXIT,  8); 
   Command info=new Command("Info" , Command.SCREEN,9);

   public main()
  {
     ...

CommandAction aus dem Interface CommandListener: was soll gemacht werden?

Ein Interface stellt uns einen Satz von Methoden(-skeletten, d.h. sie sind leer!) zur Verfügung, die wir nach unseren Wünschen gestalten (mit Fleisch füllen) können, Fachausdruck dafür: wir überschreiben die Methode xyz().

Wenn nun jemand ein Menupunkt oder ein sonstiges Kommando gewählt hat, stößt der CommandListener diese Methode an. Dabei wird übergeben, was für ein Kommando gewählt wurde, und von welchem Bildschirm aus es geschah:
public void commandAction(Command cmd,Displayable d)

Warum public? Ausserhalb von unserem package geht das doch niemanden was an!
Der Grund ist, diese Methode ist frei für alle, und wir können nicht einfach diese Freiheit durch Überschreiben beschneiden!
Vergleich: verarbeitet mal Code, der unter GPL2 steht, in ein proprietäres Projekt. Wenn das auffällt, gibt es ein ähnliches Geschrei wie das, was der Compiler bei solchen Versuchen ablässt.

Na, dann packen wir mal Fleisch an die Knochen. Wir weisen jedem Kommando eine nette kleine Methode zu, die wir später noch schreiben werden.

  public void commandAction(Command cmd,Displayable d)
  {
    if (cmd==help) show_help();
    else if (cmd==neu ) starte_spiel();
    else if (cmd==lade) lade_spiel(); 
    else if (cmd==save)  speichere_spiel();
    else if (cmd==cont)  fortsetzung();
    else if (cmd==hsc)  show_hsc();
    else if (cmd==hireset)  loesche_hsc();
    else if (cmd==info)  show_info();
    else if (cmd==exit)
    {
        try
        { 
            destroyApp(false);   // letzten Wunsch erfüllen
            notifyDestroyed();    // Langoliers rufen
        }
        catch (MIDletStateChangeException ex) { } // Notbremse
     }
  }


zur Inhaltsangabe

7. MIDP-Hi-Level UI: Form

Ein Midlet hat genau eins und nur ein Display, das Handle darauf kann man sich mit getDisplay() besorgen. Obendrein gibt es Displayable (Objects), Form und Canvas gehören dazu. Natürlich kann nur eines dargestellt werden, die Auswahl erfolgt mit setCurrent(). Man sollte bedenken, dass trotz setCurrent der gewünschte Bildschirm von anderen Applikationen verdeckt werden kann. Die wird später für den Tetrix-Timer berücksichtigt, wenn beim Canvas (Low Level UI) mit showNotify und hideNotify die Stoppuhr gestartet oder angehalten wird.

Ich besorge mir das Handle aufs Display im Midlet-Konstruktor.

Textdarstellung mit Form

Wir wählen das Displayable Form zum Darstellen der Hilfstexte aus. Es bringt als MIDP-Hi Level UI jede Menge Bequemlichkeiten mit sich wie Scrolling-Funktionen, wenn der Text nicht passt (KRZR und Emulator), eine Liste (Items) zum Einhängen der darzustellenden Elemente, alles Schönheiten, die man beim Canvas selbst erstellen muss.

Dafür kann man sich abschminken, Fonts, Textfarben und Hintergrundfarben zu setzen, die erscheinen in den von den Handy-Herstellern für uns ausgesuchten Farbe und Form, beim WTK-Emulator mit zierlicher, schwarzer Schrift auf weiss, beim Kratzer mit gut lesbarem Font, aber weniger Platz für Text, gelb auf Mittel- Dunkelblau. Mit etwas Glück kann man festlegen, ob eine Textpassage in einer anderen Fontgröße erscheinen soll.

Gibt man nichts Besonderes an, werden die Items einer Form untereinander angeordnet, was bei dem bisschen Platz auf einem Handy-Display ganz sinnvoll ist. Items können Textpassagen und Bilder sein, und so gönnte ich meinen ersten Programmen einen netten Begrüßungsbildschirm mit einer Nachrichtenbox. Dies sah im Emulator auch schick aus, der KRZR mopste mir für die UI-Elemente jedoch so viel Platz, dass dieser Screen ein Witz war, und ich dann lieber direkt zum Spiel umgeschaltet habe.

alter Begrüßungs-Screen:

  static Display d;           // wird im Konstruktor belegt
  Form titlescreen=null;
  ImageItem titelbild=null;
  String message="Hallo Zocker, suche aus dem Menu was aus!";
  ...

  public main()
  { 
    d=Display.getDisplay(this);
    ...
  }

// Titlescreen, sun 2002-konform:
  void show_title()
  { Image img=null;

     if (titlescreen == null)
     {
       titlescreen = new Form("FlowerPower");
       try
       {
         img=Image.createImage("/images/titel.png");
         titelbild=new ImageItem("",canvas.hole_bild("/images/titel.png"),0,"Titelbild");
         titlescreen.append(titelbild);
       }
       catch(IOException ioe)
       {
         form.append("Title image not available!");
       } 
       titlescreen.append(message);

       titlescreen.addCommand(help);
       titlescreen.addCommand(neu);
       titlescreen.addCommand(hsc);
       titlescreen.addCommand(hireset);
       titlescreen.addCommand(info);
       titlescreen.addCommand(exit);
     }
     titlescreen.setCommandListener(this);
     d.setCurrent(titlescreen);
  }

Das Bild habe ich bis auf die Höhe von 96 Pixeln eingekürzt, auf dem Emulator sah es mit dem weissen Hintergrund bescheiden aus, auf dem KRZR auch nicht viel besser. Man bedenke: das KRZR hat eine Bildhöhe von 220px, aber das User interface mopst so viel, dass z.B. vom Canvas ca. 176px übrigbleiben. Von der Form gehen nochmal etliche px ab für den Titel und einen zusätzlichen, seltsamen weissen Rand in Titelgröße, es sah grausam aus! Zudem nahm das Bild eine Menge Platz im Jar weg, also raus damit!

Aber obiges Beispiel zeigt, wie enfach es ist, Text unterzubringen: man appendet einfach einen String an die Form.

Das Displayable-Element Form bekommt seine Kommandos verpasst (addCommand), zu guter Letzt bekommt es den CommandListener zugesprochen (ohne das ist es taub!), dann wird mit d.setCurrent versucht, es in den Vordergrund zu holen.

Der Info-Text wurde mit so einem Form-Bildschirm erstellt.

Lesen aus JAR (BlockRead)

Für umfangreiche Texte oder Texte, die sich oft ändern, sollte man den Text nicht "einkompilieren" sondern extern halten. Das erleichtert einige Compilationen. Der Klartext wird dann mit ins JAR gepackt.

Der Ordnung halber stecken wir den Text zu den Levells, also nach (app)/res/data.

Es wird zwar empfohlen, als Zeichensatz UTF-8 zu nehmen. Der Kratzer kann damit auch umgehen, aber andere Handys möglicherweise nicht. Deshalb ist es Usus, statt ä, ö und ü ae, oe und ue zu nehmen.

Das Einlesen des Textes erfolgt (fast) wie bei den Leveln, aber statt in ein Integer-Array lesen wir das Ganze in ein 4k-großes Byte-Array ein. gelesen=InputStream is.read(b) versucht, dieses Byte-Array zu füllen und gibt die Anzahl der gelesenen Bytes zurück. Das ganze Byte-Array b wird vom Byte 0 an mit allen n gelesenen Bytes als String in den String-Puffer out geschoben. out.append(new String(b, 0, n)); (=BlockRead)

Da es passieren kann, dass man mehr als 4k zusammenschreibt, wird der Vorgang so lange wiederholt, und der String-Puffer so lange vergrößert, bis nichts mehr gelesen wird.

Aus diesem holen wir uns letztendlich den benötigten String mit toString() raus.

Warum dieser Umstand?

Schon in Turbo Pascal war BlockRead gegenüber "normalen" Read-Operationen sauschnell. Besonders bemerkbar macht sich das bei Programmen zur Logauswertung, zudem ist die Dateilänge kein Hindernis mehr

// Helpscreen:
  void show_help()
  { String t=""; // Ausgabe-String
    byte b[]=new byte[4096]; // Vorrat, in dem das ganze File reinpassen sollte
    if (helpscreen == null)  // sun-2002-konform
    { helpscreen = new Form("Tetrix - Help"); // Titelzeile
       InputStream is=null;
       try 
       { is = getClass().getResourceAsStream("/data/help.txt");
         if (is != null)
         { StringBuffer out = new StringBuffer(); // StringBuffer mit 16 Plätzen Reserve
           for (int n; (n = is.read(b)) != -1;) { out.append(new String(b, 0, n)); }
           t=out.toString();
           is.close();
         } 
         else t="Could not find help data";
       }
       catch (java.io.IOException ex) {  }
      helpscreen.append(t);
      helpscreen.append(message);

      // die ganzen Kommandos wieder anfügen:
      helpscreen.addCommand(neu);
      helpscreen.addCommand(lade);
      helpscreen.addCommand(hsc);
      helpscreen.addCommand(hireset);
      helpscreen.addCommand(info);
      helpscreen.addCommand(exit);
    }
    if (canvas.my_game.spiel_laeuft) helpscreen.addCommand(cont);
    helpscreen.setCommandListener(this);
    d.setCurrent(helpscreen);
  }

8. MIDP-Low-level UI Canvas

Klasse canvas, das UI fürs Spiel: Graphic, Tastaturbelegung, einfacher Refresh-Thread

9. Klasse game, Berechnungen fürs Spiel

Klasse game, der Rechenknecht, Spielfeld- und Animationskontrolle via Thread, Timer, Reaktion auf Tasten vom UI

10. Klasse chip, die Tetrix-Steine

Codierung Tetrix-Steine, Vorbild: Gardners Tetrominos

11. Highscore-Liste

Hiscore-Canvas, noch'n UI - Überlegungen zur Texteingabe, Hiscore und loadsave: RecordStore