![]() |
|
Zur praktischen Anwendung der Kenntnisse, die in diesem Kapitel vermittelt wurden, wird in diesem Teil des Kapitels erläutert, wie die Benutzeroberflächen des Clients des Spiels „Schiffe versenken" implementiert werden können. Im Einzelnen sind hierzu die die folgenden Aufgaben zu realisieren:
Im Folgenden werden nach der Erklärung der Klassenhierarchie diese Aufgaben einzeln vorgestellt. Es sei darauf hingewiesen, dass das mit dem Java-AWT implementierte GUI in den folgenden Kapiteln durch Swing bzw. JavaBeans ausgetauscht wird, um die Unterschiede und Gemeinsamkeiten der verschiedenen Ansätze verdeutlichen zu können. Klassenhierarchie Zur Realisierung der Oberflächen wird die in Abb. 4-38 angegebene Klassenhierarchie verwendet. Hierbei sind Klassenbeziehungen mit durchgezogenen Linien gekennzeichnet, während Aufrufe durch gestrichelte Linien dargestellt sind. Aufgabe des Hauptprogramms ist es, das Steuerungs-GUI zu erstellen, in dem neben Buttons Instanzen der Klassen SpielCanvas (Anzeige der Schiffe des Clients) und SpielCanvasPos1 (Anzeige der Schiffe des Servers mit zusätzlicher Verarbeitung von Maus-Events) enthalten sind. Die eigentliche Funktionalität der Klassen SpielCanvas und SpielCanvasPos1 ist in den Klassen SelectionArea und SelectionAreaPos1 verborgen. Zur Verwaltung der Spielfeldinformation bzw. der Informationen über die Schiffskoordinaten werden Instanzen der Klasse Spielfeld erzeugt, die ihrerseits die notwendigen Schiffsinformationen von der Klasse Schiffe erbt. Das Aufstellen der Schiffe ist in der Klasse SchiffFenster realisiert, die über die Klasse SpielCanvasPos2 auf die Klasse SelectionAreaPos2 zugreift und so das Aufstellen und Entfernen von Schiffen inklusive einer ausgefeilten Fehlerlogik ermöglicht. Treten hierbei Fehler auf, so wird ein Pop-Up-Fenster angezeigt, das in der Klasse FehlerFenster realisiert wird. Zur Anzeige, welche Schiffe bereits positioniert sind bzw. welche noch aufzustellen sind, werden Instanzen der Klasse Quadrat als Teil des GUIs SchiffFenster erzeugt, die entweder ein grünes Quadrat (Schiff aufgestellt) oder ein rotes Quadrat (Schiff noch zu platzieren) anzeigen. Hauptprogramm Aufgabe des Hauptprogramms, das in Form eines Applets in der Klasse GUISVUser implementiert ist, ist die Steuerung der gesamten Client-Anwendung. Dieses soll im Folgenden Schritt für Schritt durchgegangen werden. Setzt man die Code-Stücke wieder zusammen, so erhält man das übersetzungsfähige Programm.
import java.applet.*; SpielCanvasPos1 sc1; Zunächst werden in dieser Klasse verschiedene Klassenvariablen deklariert. Hierzu zählen neben den grafischen Elementen die Datenstrukturen spieler und computer, die die Spielfelder des Spielers bzw. die des Rechners verwalten. In der Variablen c wird eine Referenz auf das Applet selbst gespeichert. Anschließend wird die Hauptoberfläche in der Methode init definiert.
public void init () { c = this; Nachdem die Referenz auf das Applet selbst in der Variablen c gespeichert wurde, werden die Datenstrukturen des Rechners und des Spielers initialisiert. Im Anschluss daran werden für das Applet ein Grid-Layout, die Größe und die Hintergrundfarbe festgelegt. Hierbei ist anzumerken, dass die Hintergrundfarbe weiß nun als Integer-Zahl (16777215) dargestellt ist. In der hexadezimalen Darstellung müsste hierbei die Kombination 0xFFFFFF angegeben werden. Im Anschluss daran werden zuerst die Buttons angelegt, mit denen die Schiffe des Benutzers platziert werden können bzw. mit denen auf Schiffe des Rechners geschossen werden kann.
// Setzen der Buttons buttonPanel.setLayout(new FlowLayout(FlowLayout.CENTER,5,5)); buttonPanel.setBounds(0,0,142,266); buttonPanel.setBackground(new Color(16777215)); add(buttonPanel); setship = new Button(); setship.setLabel("Neues Spiel"); setship.addActionListener(new ButtonListener(0)); setship.setBounds(31,5,80,23); setship.setBackground(new Color(12632256)); buttonPanel.add(setship); shoot = new Button("Schuss abgeben"); shoot.setEnabled(false); shoot.addActionListener(new ButtonListener(1)); shoot.setBounds(17,33,108,23); shoot.setBackground(new Color(12632256)); buttonPanel.add(shoot); Die Buttons werden in einem Panel (buttonPanel) platziert. Für das Panel und die Buttons werden wiederum die Größe und der Hintergrund angegeben. Zusätzlich werden die Bezeichnungen und Listener-Objekte der Buttons angegeben. Zur Unterscheidung, welcher Button gedrückt wurde, wird ein Parameter übergeben. Wird der Button setShip betätigt, so hat der Parameter den Wert 0, für den Button shoot den Wert 1. Nach Platzierung der Buttons werden die Spielfelder des Rechners und des Benutzers angelegt.
//Spielfeld des Computers sc1Panel.setLayout(new BorderLayout(0,0)); sc1Panel.setBounds(142,0,142,266); sc1Panel.setBackground(new Color(16777215)); sc1 = new SpielCanvasPos1(computer); sc1Panel.add("Center",sc1); label1 = new Label("Spielfeld des Computers", Label.LEFT); label1.setBounds(0,133,142,133); sc1Panel.add("South",label1); add(sc1Panel); //Spielfeld des Benutzers } Object anker = getParent(); anker = ((Component) anker).getParent(); FehlerFenster dialog = new FehlerFenster((Frame)anker, s); } Für den Rechner wird ein Spielfeld sc1 vom Typ SpielCanvasPos1 angelegt, mit dem ein Maus-Listener verbunden ist, der dem Spieler das Schießen auf Koordinaten des Rechners ermöglicht. Der Namensteil Pos1 soll hierbei verdeutlichen, dass der Spieler jeweils eine Koordinate angeben kann. Im Gegensatz dazu ist ein Listener für das Spielfeld des Benutzers (sc2) nicht notwendig. Hier soll lediglich die Position der Schiffe des Benutzers wiedergegeben werden, die nicht mehr verändert werden kann. Den Abschluss der Hauptdatei bildet die Klasse ButtonListener, die das Interface ActionListener implementiert. Da diese Klasse nur innerhalb der Datei eine Bedeutung hat, ist sie als innere Klasse implementiert. Nachdem zunächst im Konstruktor die Parameterinitialisierung definiert wird, ist in der Methode actionPerformed festgelegt, welche Aktion in Abhängigkeit vom auslösenden Button erfolgen soll. Wurde der Button setShip betätigt, so wird ein neues Fenster erzeugt, mit dessen Hilfe der Benutzer seine Schiffe im Spielfeld platzieren kann. Hierzu werden die Datenstruktur, in der das Spielfeld des Benutzers gespeichert ist und eine Referenz auf das Applet selbst als Parameter übergeben. Weiterhin kann der Button, der das Schießen ermöglicht, nach dem Setzen der Schiffe aktiviert werden. Wird der Button shoot betätigt, so wird die Methode getSchussKoordinaten aufgerufen, die feststellt, auf welche Position des Rechnerspielfeldes der Benutzer feuern möchte.
class ButtonListener implements ActionListener { private int val; this.val = val; } Point p = null; spieler.initialisieren(); } else{ //Schiessen getSchussKoordinaten(); return; shoot.setEnabled(false); } validate(); } System.out.println("Schuss auf "+sc1.sa1.tmp.x+" "+sc1.sa1.tmp.y); } } } Klasse Schiffe Aufgabe der Klasse Schiffe ist es, ein Schiff mittels der Anfangs- bzw. Endkoordinaten zu verwalten. Der hierzu notwendige Code ist sehr einfach:
public class Schiffe { Point [] p; p = new Point[2]; } } Klasse Spielfeld Die Klasse Spielfeld stellt Funktionen zur Verfügung, mit denen die Spielfelder des Servers und des Clients verwaltet werden können. Neben Konstanten und Variablen zählen hierzu die Initialisierung der Spielfelder und die Verwaltung der einzelnen Koordinaten. Hierbei ist zu bedenken, dass bspw. Felder, die zu denen eines bereits belegten Schiffes benachbart sind, nicht durch ein weiteres Schiff belegt werden dürfen. Zuerst werden die Konstanten und die notwendigen Klassenvariablen angelegt.
public class Spielfeld extends Schiffe { //Konstanten Während die Konstanten WASSER, SCHIFF, FREI und VERSENKT selbsterklärend sind, wird die Konstante BELEGT dazu verwendet, um Positionen zu markieren, an denen keine weiteren Schiffe aufgestellt werden dürfen. In der Variablen spiel wird das Spielfeld gespeichert, in schiffZahl die Anzahl der noch nicht getroffenen Schiffspositionen und in der Liste schiff die Schiffe mit Anfangs- und Endposition. Es ist nun leicht feststellbar, ob noch weitere Schiffe aufgestellt werden müssen bzw. ob bereits alle Schiffe des Spielers oder des Rechners getroffen wurden, indem die Variable schiffZahl ausgewertet wird.
public Spielfeld() { int i,j; schiff[i]= new Schiffe(); spiel = new int[10][10]; } Im Konstruktor werden die Schiffe initialisiert. Weiterhin wird das Spielfeld definiert und in der Methode initialisieren mit Anfangswerten belegt. Dies könnte zwar auch innerhalb des Konstruktors erfolgen, man würde damit aber den Nachteil in Kauf nehmen, dass für jedes neu gestartete Spiel eine neue Instanz der Klasse Spielfeld angelegt werden müsste. Auf diese Art und Weise jedoch kann aus dem Hauptprogramm die Methode initialisieren aufgerufen werden, wobei die bereits existierende Instanz der Klasse Spielfeld weiter verwendet werden kann. Aufgabe der nun folgenden Methode initialisieren ist es daher, die Positionen des Spielfelds mit dem Wert FREI zu belegen, bzw. die Anfangs- und Endpositionen der Schiffe auf den Wert -1 zu setzen. Hierbei wird angenommen, dass Schiffe, deren Positionsangaben aus dem Wert -1 bestehen, noch nicht belegt wurden.
public void initialisieren(){ //initialisiere Spielfelder als leer for (int i = 0; i<10; i++) for (int j=0;j<10;j++) spiel[i][j]=FREI; for (int i = 0; i<5; i++){ schiff[i].p[0].x=schiff[i].p[0].y=-1; } } In der nun folgenden Methode wird das Spielfeld reorganisiert. Diese Funktion muss jedes Mal ausgeführt werden, wenn ein neues Schiff gesetzt wurde, bzw. wenn ein Schiff gelöscht wurde. Beim Löschen reicht es nicht aus, die Felder des Schiffs von SCHIFF auf FREI zu setzen, da dann die um das Schiff belegten Felder unzulässig weiterhin mit dem Wert BELEGT gekennzeichnet wären. Andererseits können aber auch diese Felder nicht ohne weiteres freigegeben werden. Sind zwei Schiffe übereinander angeordnet, so würde das Freigeben der BELEGT-Felder bewirken, dass die Felder rund um das eigentlich nicht veränderte zweite Schiff freigegeben würden.
public void reorganisiereSpielfeld() { int i,j; for (j=0;j<10;j++) if (spiel[i][j]!=SCHIFF) spiel[i][j]=FREI; //Felder um ein Schiff herum duerfen nicht mit einem for (i=0;i<10;i++) for (j=0;j<10;j++) if (spiel[i][j]==SCHIFF){ if (moeglichePosition(i-1,j-1)) spiel[i-1][j-1]=BELEGT; if (moeglichePosition(i-1,j)) spiel[i-1][j]=BELEGT; if (moeglichePosition(i-1,j+1)) spiel[i-1][j+1]=BELEGT; if (moeglichePosition(i,j-1)) spiel[i][j-1]=BELEGT; if (moeglichePosition(i,j+1)) spiel[i][j+1]=BELEGT; if (moeglichePosition(i+1,j-1)) spiel[i+1][j-1]=BELEGT; if (moeglichePosition(i+1,j)) spiel[i+1][j]=BELEGT; if (moeglichePosition(i+1,j+1)) spiel[i+1][j+1]=BELEGT; } return;} private boolean moeglichePosition(int x, int y) { boolean test = false; if (spiel[x][y]!=SCHIFF) return true; return test; } Die Logik dieser Methode ist sehr einfach. Zuerst werden alle Felder, auf denen kein Schiff steht, freigegeben. Anschließend wird für jede Position, an der ein Schiff vorzufinden ist, geprüft, ob die angrenzenden Positionen innerhalb des Spielfelds liegen und ob sich dort kein weiteres Feld eines Schiffes befindet. Ist dies der Fall, so wird die jeweilige Position mit dem Wert BELEGT gekennzeichnet. Sind einmal alle Schiffe platziert, so werden die BELEGT-Felder nicht länger benötigt. In der Methode spielfeldFertigstellen werden diese Felder daher wieder freigegeben.
public void spielfeldFertigstellen() { int i,j; for (j=0;j<10;j++) if (spiel[i][j]!=SCHIFF) spiel[i][j]=FREI; return; } } Klasse SpielCanvas Die Klasse SpielCanvas hat die Aufgabe, ein Spielfeld zu zeichnen und die Positionen in Abhängigkeit von der Datenstruktur Spielfeld einzufärben, bspw. grün für Schiff oder blau für Wasser. Diese Funktionalität wird realisiert, indem die paint-Methode überschrieben wird. Das Spielfeld wird hierbei als Parameter an den Konstruktor übergeben.
import java.awt.*; Spielfeld s; super(); } Das Zeichnen eines Spielfeldes wurde bereits im Laufe dieses Kapitels erläutert.
public void paint(Graphics screen) { int i,j; screen.drawLine(0, 0+i*20,200, 0+i*20); } Anschließend werden die Positionen in Abhängigkeit der Belegung mit Konstanten eingefärbt.
// Markiere Schiffe und Wasser for (j = 0; j < 10; j++) if (s.spiel[i][j]!= s.FREI){ switch (s.spiel[i][j]) { case s.WASSER: // Erzeuge blaues Quadrat break; case s.SCHIFF: // Erzeuge gruenes Quadrat break; case s.VERSENKT: // Erzeuge rotes Quadrat break; default: break; } } } } Klasse SpielCanvasPos1 Die durch die Klasse SpielCanvas realisierte Funktionalität umfasst keinerlei Verarbeitung von Maus-Events. Hierzu stehen die Klassen SpielCanvasPos1 und SpielCanvasPos2 zur Verfügung. Die in der Klasse SpielCanvasPos1 enthaltene Funktionalität umfasst die Verarbeitung einzeln anfallender Maus-Events, wie sie beim Markieren von Schusspositionen anfallen.
import java.applet.*; SelectionAreaPos1 sa1; super(s); } } Es ist leicht erkennbar, dass diese Klasse die Positionierung eines SelectionAreaPos1-Objekts implementiert, das im Folgenden beschrieben wird. Klasse SpielCanvasPos2 Die Aufgabe der Klasse SpielCanvasPos2 ist ähnlich der der Klasse SpielCanvasPos1. Unterschiedlich ist allerdings, dass die hier eingebettete Klasse SelectionAreaPos2 Anfangs- und Endpositionen von Schiffen erwartet.
import java.applet.*; public SpielCanvasPos2 (Spielfeld s, Quadrate[] q) { super(s); } } Klasse SelectionArea Die Klasse SelectionArea hat (wie bereits die Klasse SpielCanvas) die Aufgabe, das Spielfeld zu zeichnen. Zusätzlich dazu wird aber auch eine Koordinate, die ein Benutzer markiert hat, als grünes Feld eingetragen.Hierzu ist es notwendig, die Koordinate, die in der Variablen tmp gespeichert wird, anfangs mit den Werten (-1, -1) zu belegen, damit keine Felder angezeigt werden, bevor der Benutzer eine Koordinate markiert hat.
import java.awt.*; Point tmp; super(); } int i,j; screen.drawLine(0, 0+i*20,200, 0+i*20); } for (j = 0; j < 10; j++) if (s.spiel[i][j]!= s.FREI){ switch (s.spiel[i][j]) { case s.WASSER: // Erzeuge blaues Quadrat break; case s.SCHIFF: // Erzeuge gruenes Quadrat break; case s.VERSENKT: // Erzeuge rotes Quadrat break; default: break; } } //Zeichne Koordinate ein screen.setColor(Color.green); } } } Klasse SelectionAreaPos1 Die Klasse SelectionAreaPos1 erweitert die Klasse SelectionArea um einen Maus-Listener, mit dessen Hilfe Positionen des Spielfelds markiert werden können.
import java.applet.*; import java.awt.*; MyListener myListener; super(s); } public void mousePressed(MouseEvent e) { if ((tmp.x = (int)e.getPoint().x/20 ) > 9) tmp.x=9; if ((tmp.y = (int)e.getPoint().y/20 ) > 9) tmp.y=9; repaint(); } } } Klasse SelectionAreaPos2 Die Klasse SelectionAreaPos2 ist ohne Zweifel die komplexeste Klasse dieses Anwendungsbeispiels. Dies liegt unter anderem daran, dass zur intelligenten Verarbeitung der Benutzereingaben beim Aufstellen der Schiffe eine Reihe von Schritten notwendig sind. Der Anfang dieser Klasse ähnelt stark dem Aufbau der Klasse SelectionAreaPos1. Eine Ausnahme hierzu ist allerdings, dass eine Liste von Quadrate-Objekten deklariert wird, mit deren Hilfe der Benutzer erkennen kann, welche Schiffe bereits aufgestellt wurden und welche noch platziert werden müssen.
import java.applet.*; MyListener myListener; super(s); } Im Anschluss daran wird der Maus-Listener deklariert. Zu Anfang werden zwei Punkte definiert, die den Koordinaten entsprechen, die der Benutzer mit der Maus markieren soll. Hierbei sei darauf hingewiesen, dass zur Implementierung des Interfaces eine Adapterklasse verwendet wird.
class MyListener extends MouseAdapter { Point p[]; p = new Point[2]; } In der nun folgenden Methode mousePressed wird zunächst eine Koordinate in die Variable tmp eingelesen.
public void mousePressed(MouseEvent e) { if ((tmp.x = (int)e.getPoint().x/20 ) > 9) tmp.x=9; if ((tmp.y = (int)e.getPoint().y/20 ) > 9) tmp.y=9; Anschließend muss unterschieden werden, ob die Koordinate die Anfangs- oder die Endposition eines Schiffes markiert. Ist die Anfangsposition des Punktepaares ungleich -1, so muss die Endkoordinate belegt werden. Weiterhin muss unterschieden werden, ob die Koordinate noch frei ist, oder ob hier bereits ein Schiff platziert ist. Wird ein Schiff vorgefunden, so soll dieses in der Folge gelöscht werden. Anderenfalls wird die Koordinate als Anfangsposition eines neuen Schiffs aufgefasst. Treffen beide Fälle nicht zu, so liegt ein Fehler vor und die Variable tmp wird wieder auf den Wert -1 gesetzt.
//Erste Koordinate des Schiffs if ((s.spiel[tmp.x][tmp.y]== s.FREI)||(s.spiel[tmp.x][tmp.y]== s.SCHIFF)) { p[0].x = tmp.x; } else { //Falsche erste Position } repaint(); } Handelt es sich bei der angegebenen Koordinate um die Endposition eines Schiffes, so wird das Punktepaar zuerst geeignet initialisiert und dann gedreht. Es ist möglich, dass der Benutzer ein Schiff von rechts nach links, bzw. von unten nach oben angegeben hat. Um Schwierigkeiten zu vermeiden, werden die Punkte immer so gedreht, dass das Schiff von links nach rechts, bzw. von oben nach unten verläuft. Weiterhin muss hier festgestellt werden, ob der Benutzer ein bereits existierendes Schiff markiert hat, das gelöscht werden soll. Ist dies der Fall, so wird das Schiff in der Methode loescheSchiff entfernt. Anschließend wird das Spielfeld reorganisiert (siehe oben) und die Punktepaare wieder auf den Wert -1 zurückgesetzt.
else {//Zweite Koordinate p[1].x = tmp.x; loescheSchiff(p); } Im nächsten Schritt muss geprüft werden, ob die angebene Position korrekt ist. Hat der Benutzer ein diagonal verlaufendes Schiff angegeben bzw. ein Schiff einer unzulässigen Länge, so wird eine Fehlermeldung generiert. Wenn das Positionspaar jedoch korrekt eingegeben wurde, so wird das Schiff in der dazugehörigen Datenstruktur gespeichert. Im Anschluss daran muss wiederum das Spielfeld reorganisiert werden, um bspw. die Felder um das Schiff herum als belegt zu markieren. Auch die Quadrate müssen nun neu gezeichnet werden, da ja ein Schiff mehr markiert worden ist, was sich in einem Wechsel des entsprechenden Quadrats von rot nach grün auswirkt. Zum Abschluss werden die Punktepaare wieder zurückgesetzt, um die Eingabe eines weiteren Schiffes zu ermöglichen.
//ueberpruefen, ob angegebenes Positionspaar korrekt ist //Schiff im Spielfeld markieren for (int j = 0; j<=abs(p[0].y-p[1].y);j++){ if (p[0].x<p[1].x) //Horizontales Schiff else s.spiel[p[0].x][p[0].y+j] = s.SCHIFF; } s.reorganisiereSpielfeld(); q[i].repaint(); p[0].x = p[0].y = p[1].x = p[1].y = tmp.x = tmp.y = -1; Darf ein Schiff an einer angegebenen Position nicht platziert werden, so muss eine Fehlermeldung ausgegeben werden. Hierzu wird in einer while-Schleife zunächst das Fenster identifiziert, das als Elternfenster des Fehlerfensters fungieren kann. Anschließend wird eine Instanz der Klasse FehlerFenster erzeugt und dargestellt bzw. die Punktepaare wieder auf den Wert -1 zurückgesetzt.
} else { //Fehlermeldung anker = ((Component) anker).getParent(); FehlerFenster dialog = new FehlerFenster((Frame)anker, Falsche Positionsangabe!"); } } } Die Methode checkPosition überprüft, ob ein Koordinatenpaar ein Schiff in einer korrekten Art und Weise repräsentiert. Zuerst wird hierbei geprüft, ob ein Schiff mindestens zwei, aber höchstens fünf Felder lang ist, gemäß der Spielregeln des Spiels „Schiffe versenken". Anschließend wird sichergestellt, dass die Felder, auf denen das Schiff stehen soll, wirklich frei sind. Schlagen diese Prüfungen fehl, so bricht die Routine mit dem Rückgabewert false ab.
public boolean checkPosition(Point[] p) { boolean test = false; if ((abs(p[0].y-p[1].y)<=4)&& (abs(p[0].y-p[1].y)>0)) test=true; if (abs(p[0].y-p[1].y)== 0) if ((abs(p[0].x-p[1].x)<=4)&& (abs(p[0].x-p[1].x)>0)) test=true; //Sind die Schiff-Felder frei? for (int j=0;j<abs(p[0].y-p[1].y)+1;j++) if (p[0].x!= p[1].x) //Horizontales Schiff else //Vertikales Schiff if (test==false) return false; Liegt ein korrektes Punktepaar vor, so muss das entsprechende Schiff eingerichtet werden. Hierzu wird zunächst die Länge des Schiffs festgestellt und dieses in Abhängigkeit von dessen Länge im Spielfeld platziert.
//Schiffnummer identifizieren if (abs(p[0].x-p[1].x)>0)//Horizontales Schiff else //Vertikales Schiff //Richte Schiff in Abhaengigkeit von dessen Laenge ein case 2: if (richteSchiffEin(0,p)==false) return false; break; case 3: if (s.schiff[1].p[0].x == -1){ //Erster Dreier return false; } else //Zweiter Dreier return false; break; if (richteSchiffEin(3,p)==false) return false; break; case 5: if (richteSchiffEin(4,p)==false) return false; break; default: } return test; } Aufgabe der Methode richteSchiffEin ist es, ein Schiff mit dem Punktepaar zu initialisieren. Hierbei ist zu beachten, dass die korrekte Anzahl der Schiffe eingehalten wird, dass also bspw. der Benutzer nicht 4 Dreier-Schiffe eingibt. Schlägt dies fehl, so wird der Wert false zurückgegeben und damit die Methode checkPosition insgesamt negativ beendet.
public boolean richteSchiffEin(int zahl, Point p[]) { int i,j; //Schiff bereits belegt //Schiffskoordinaten setzen } Mittels der Methode loescheSchiff wird ein Schiff wieder vom Spielfeld entfernt. Hierbei wird das Schiff mittels der Methode schiffSuche zuerst gesucht. Obgleich die Koordinaten bekannt sind, ist es der Index des Schiffes in der dazugehörigen Datenstruktur noch lange nicht, sondern erst nach Durchlauf der Methode schiffSuche. Im Anschluss daran werden die einzelnen Felder des zu löschenden Schiffs wieder freigegeben. Auch die Koordinaten der schiff-Datenstruktur werden wiederum auf den Wert -1 zurückgestellt. Nachdem ein Schiff gelöscht wurden ist, muss der Benutzer eine um eins größere Zahl an Schiffen eingeben. Es ist daher auch darauf zu achten, dass die Variable schiffZahl entsprechend der Zahl der gelöschten Felder erhöht wird. Zum Abschluss der Methode müssen auch die Quadrate neu gezeichnet werden, die die noch zu setzenden Schiffe angeben.
protected void loescheSchiff(Point p[]) { int x = 0; for (int j=0; j<abs(s.schiff[x].p[0].y-s.schiff[x].p[1].y)+1; j++) s.spiel[s.schiff[x].p[0].x+i][s.schiff[x].p[0].y+j]= s.FREI; //Werte des Schiffs zuruecksetzen q[i].repaint(); repaint(); return; } Die nun folgende Methode schiffSuche stellt fest, ob die Koordinaten, die als erster Parameter übergeben werden, dem Schiff entsprechen, dessen Index als zweiter Parameter übergeben wird.
private boolean schiffSuche (Point [] p, int i) { //Finde heraus, ob ein Wertepaar zu einem bestimmten Schiff if ((s.schiff[i].p[0].x <= p[0].x)&&(s.schiff[i].p[1].x >= p[0].x)) if ((s.schiff[i].p[0].y <= p[0].y)&&(s.schiff[i].p[1].y >= p[0].y)) return true; return false; } Den Abschluss dieser komplexen Klasse bilden die zwei Hilfsmethoden drehePunkte und abs. Mittels drehePunkte wird sichergestellt, dass Schiffe immer von links nach rechts, bzw. von oben nach unten verlaufen. Die Methode abs realisiert die Betragsfunktion, gibt also unabhängig vom Vorzeichen des Eingabeparameters stets positive Werte zurück.
private Point[] drehePunkte(Point[] p){ Point tmp = new Point(); tmp.x = p[0].x; } tmp.y = p[0].y; } } return ((a>0)?a:((-1)*a)); } } } Klasse SchiffFenster Nach der Darstellung der komplexen Event-Verarbeitung ist die Erläuterung der noch fehlenden Klassen wesentlich einfacher. Aufgabe der Klasse SchiffFenster ist die Darstellung des Fensters, mit dessen Hilfe der Benutzer die Schiffe eingeben kann.
import java.awt.*; ActionListener{ Zuerst werden die Klassenvariablen deklariert. Die Variable sc1 realisiert das Feld, mit dem der Benutzer per Maus seine Schiffe aufstellen kann. Mittels der Variablen s wird die Datenstruktur des Spielfelds verwaltet. Die Liste q der Quadrate zeigt dem Benutzer an, welche Schiffe bereits platziert sind (grünes Quadrat), bzw. welche Schiffe noch aufgestellt werden müssen. Im Anschluss daran werden eine Reihe von Variablen deklariert, mit deren Hilfe Teile der Benutzeroberfläche realisiert werden. Zum Schluss der Variablendefinition wird eine Container-Variable c angegeben, die die Referenz auf das Applet speichert.
Panel southPanel; // für die zwei Informationszeilen Label label1; //1. Informationszeile Label label2; //2. Informationszeile Label label3; // Label der aufzustellenden Schiffe // Das allgemeine Panel, auf dem ein Schiff // mit seinem Bezeichner dargestellt wird Panel panel1; // Panel, worauf die Schiffe mit Bezeichnungen sowie // der "Fertig"-Button stehen Panel panel2; Container c; Im Konstruktor wird zunächst das Spielfeld initialisiert bzw. eine neue Liste an Kontrollkästchen erzeugt. Anschließend werden die generellen Eigenschaften dieser Benutzeroberfläche angegeben.
public SchiffFenster(Spielfeld s, Container c) { this.s = s; Im Anschluss wird das Spielfeld des Benutzers erzeugt. Hierzu müssen die Datenstruktur des Spielfelds und die Liste der Quadrate als Parameter übergeben werden, da aus dem Spielfeld heraus (bspw. beim Löschen der Schiffe) die Färbung der Kontrollkästchen verändert wird.
//Spielfeld des Benutzers centerPanel.add(sc1); Hierauf folgt das Anlegen der Schiffsbezeichner bzw. der Kontrollkästchen mittels der Methode zeigeQuadrat, die als Parameter einen String (den Schiffsbezeichner), einen boole'schen Wert und die Ordnungszahl des Schiffes erwartet. Der boole'sche Wert legt hierbei über den Wert false fest, dass in der ersten Zeile der Beschriftungen zusätzlich der Button fertig angezeigt wird, mit dem der Benutzer mitteilt, dass er alle Schiffe aufgestellt hat.
//Infofelder panel2.setLayout(new GridLayout(5,1,3,3)); centerPanel.add(panel2); zeigeQuadrat("2-er Schiff", false, 0); zeigeQuadrat("3-er Schiff 1", true,1); zeigeQuadrat("3-er Schiff 2", true, 2); zeigeQuadrat("4-er Schiff", true, 3); zeigeQuadrat("5-er Schiff", true, 4); Den Abschluss der Erstellung des GUIs bilden zwei Textmarken, in denen dem Benutzer mitgeteilt wird, wie er Schiffe setzen und entfernen kann. Auch diese Komponenten werden über Panels gruppiert.
southPanel = new Panel(); southPanel.setBounds(0,220,426,0); add("South", southPanel); label1 = new Label("Zum Setzen der Schiffe bitte Anfangs- und Endposition mit der Maus markieren", Label.LEFT); label1.setBounds(0,0,426,23); southPanel.add(label1); label2 = new Label("Zum Entfernen der Schiffe bitte Anfangs- und Endposition nochmals mit der Maus markieren", Label.LEFT); label2.setBounds(0,23,426,23); southPanel.add(label2); } In der nun folgenden Methode zeigeQuadrat werden der Bezeichner eines Schiffes und das Kontrollkästchen gruppiert. Zusätzlich wird in der ersten Zeile der Button Fertig erzeugt, mit dessen Hilfe das Fenster wieder geschlossen werden kann (erst, wenn alle Schiffe aufgestellt sind).
protected void zeigeQuadrat(String name, boolean val, int schiffNummer) { panel1 = new Panel(); Button fertig = new Button("Fertig"); } } Das Event-Handling bildet den Abschluss dieser Klasse. Jedes Mal, wenn der Benutzer den Fertig-Button betätigt, muss geprüft werden, ob bereits alle Schiffe aufgestellt wurden. Ist dies der Fall, so wird das Fenster geschlossen und das Spiel kann beginnen. Anderenfalls wird eine entsprechende Fehlermeldung ausgegeben. Die Ausgabe der Fehlermeldung erfolgt analog wie zu dem Fall, in dem ein Schiff ungültig gesetzt wird.
public void actionPerformed(ActionEvent e) { if (s.schiffZahl == 0){ repaint(); } else { Object anker = sc1.getParent(); anker = ((Component) anker).getParent(); FehlerFenster dialog = new FehlerFenster((Frame)anker, "Erst alle Schiffe setzen!!"); } } } Klasse Quadrate Mittels der Klasse Quadrate werden hinter den Schiffsbezeichnern kleine Kästchen dargestellt, die entweder rot oder grün gefärbt sind. Ist ein derartiges Quadrat rot gefärbt, so muss der Benutzer das dazugehörige Schiff noch setzen. Wurde ein Schiff bereits platziert, so ist das entsprechende Quadrat grün.
import java.awt.*; int nummer; super(); } int i; screen.setColor(Color.green); else screen.setColor(Color.red); //Das eigentliche Quadrat muss die Koordinaten (1,1,14,14), } } Klasse FehlerFenster Aufgabe der Klasse Fehlerfenster ist es, ein Pop-Up-Fenster darzustellen, mit dessen Hilfe eine Fehlermeldung angezeigt wird. Das Fenster besteht aus einem Titel, in dem die Fehlermeldung erscheint, sowie aus einem Cancel-Button, mit dessen Hilfe das Fenster wieder geschlossen wird. Wenn ein Event ausgelöst wird, wenn also der Button betätigt wird, wird das Fenster wieder geschlossen.
import java.awt.*; ActionListener{ public FehlerFenster(Frame dw, String title) { super(dw, title, false); } setVisible(false); } } Ausführung der Anwendung Nach Übersetzung der Klassen (javac *.java) kann das Programm mittels der Anweisung appletviewer GUISVUser.html ausgeführt werden, wenn eine dementsprechende Webseite erstellt wurde. Sicherlich ergibt sich auch hieraus kein besonders sinnvolles Spiel, da die Server-Komponenten fehlen. Der Client kann zwar seine Schiffe setzen, anschließend ist das Spiel aber vorbei. Das vollständige Spiel ist in Kapitel 7 beschrieben.
Abb. 4.39: Hauptfenster In Abb. 4-39 ist das Hauptfenster des Applets abgebildet, in Abb. 4-40 die Benutzeroberfläche, in der der Anwender seine Schiffe aufstellen kann.
Abb. 4.40: Eingabe der Schiffe
|
|
|