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: Implementierung des Hauptfensters des Clients, von dem aus die Aufstellung der Schiffe aufgerufen wird. Weiterhin müssen die Spielfelder des Clients und des Servers (soweit durch Schüsse bekannt) aufgestellt werden und der Client in die Lage versetzt werden, Schüsse auf das Spielfeld des Servers abzufeuern. Aufstellen der Schiffe des Clients. Hierbei gilt die Regel, dass ein Schiff aus zwei Elementen, zwei aus drei Elementen, eines aus vier Elementen und ein Schiff, das aus fünf Elementen besteht, aufgestellt werden können. Schiffe dürfen nur horizontal oder vertikal platziert werden und dürfen sich weiterhin nicht berühren. Der Client sollte die notwendigen Positionen mit der Maus in einem Spielfeld markieren und einmal aufgestellte Schiffe durch erneute Markierung mit der Maus wieder löschen können. Schießen auf Schiffe des Servers nach der Regel, dass jeweils nach einem Treffer ein weiterer Schuss abgegeben werden darf. 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. Abb. 4.38: Klassenhierarchie des Beispiels 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.*; import java.awt.*; import java.awt.event.*; public class GUISVUser extends Applet { SpielCanvasPos1 sc1; SpielCanvas sc2; Spielfeld spieler, computer; Button shoot, setship; // Panel, das Buttons (shoot und setship) enthaelt Panel buttonPanel; // im Zentrum dieses Panels wird das Spielfeld des Computers //platziert Panel sc1Panel; // im Zentrum dieses Panels wird das Spielfeld des Spielers //platziert Panel sc2Panel; Label label1; // Bezeichnung des Spielfelds des Computers Label label2; // Bezeichnung des Spielfelds des Spielers Container c; 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; computer = new Spielfeld(); spieler = new Spielfeld(); setLayout(new GridLayout(1,3,5,0)); setSize(650,266); setBackground(new Color(16777215)); 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 = new Panel(); 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 = new Panel(); 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 sc2Panel = new Panel(); sc2Panel.setLayout(new BorderLayout(0,0)); sc2Panel.setBounds(284,0,142,266); sc2Panel.setBackground(new Color(16777215)); add(sc2Panel); sc2 = new SpielCanvas(spieler); sc2Panel.add("Center",sc2); label2 = new Label("Spielfeld des Benutzers", Label.LEFT); label2.setBounds(0,133,142,133); sc2Panel.add("South",label2); } public void ausgabeFenster (String s) { Object anker = getParent(); while (! (anker instanceof Frame)) anker = ((Component) anker).getParent(); FehlerFenster dialog = new FehlerFenster((Frame)anker, s); dialog.setVisible(true); dialog.setSize(400,100); return; } 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; String []s; ButtonListener (int val) { this.val = val; } public void actionPerformed(ActionEvent e) { Point p = null; if (val == 0){ //Schiffe setzen spieler.initialisieren(); computer.initialisieren(); SchiffFenster fenster = new SchiffFenster(spieler, c); fenster.setTitle("Schiffe setzen"); fenster.pack(); fenster.setVisible(true); fenster.setSize(600,350); shoot.setEnabled(true); repaint(); } else{ //Schiessen getSchussKoordinaten(); //Koordinaten bereits markiert? if (sc1.sa1.tmp.x == -1) return; shoot.setEnabled(false); } validate(); } public void getSchussKoordinaten() { 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; public Schiffe () { p = new Point[2]; p[0] = new Point(-1,-1); p[1] = new Point(-1,-1); } } 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 public final int WASSER = 0; public final int SCHIFF = 1; public final int BELEGT = 2; public final int FREI = 3; public final int VERSENKT = 4; //Variablen public int spiel[][]; public int schiffZahl; Schiffe[] schiff; 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 = new Schiffe[5]; for (i=0;i<5;i++) schiff[i]= new Schiffe(); spiel = new int[10][10]; initialisieren(); schiffZahl=17; } 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; schiff[i].p[1].x=schiff[i].p[1].y=-1; } return; } 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 (i=0;i<10;i++) for (j=0;j<10;j++) if (spiel[i][j]!=SCHIFF) spiel[i][j]=FREI; //Felder um ein Schiff herum duerfen nicht mit einem //weiteren Schiff belegt werden 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 ((x>=0)&&(x<10)&&(y>=0)&&(y<10)) 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 (i=0;i<10;i++) 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.*; import java.awt.event.*; public class SpielCanvas extends Panel { Spielfeld s; public SpielCanvas (Spielfeld s) { super(); this.s = s; } Das Zeichnen eines Spielfeldes wurde bereits im Laufe dieses Kapitels erläutert. public void paint(Graphics screen) { int i,j; // Erzeuge Quadrat als Spielfeldbegrenzung screen.setColor(Color.white); screen.fillRect(0,0,200,200); screen.setColor(Color.black); screen.drawRect(0,0,200,200); // Erzeuge horizontale und vertikale Linien for (i = 1; i < 10; i++){ screen.drawLine(0, 0+i*20,200, 0+i*20); screen.drawLine(0+i*20, 0,0+i*20, 200); } Anschließend werden die Positionen in Abhängigkeit der Belegung mit Konstanten eingefärbt. // Markiere Schiffe und Wasser for (i = 0; i < 10; i++) for (j = 0; j < 10; j++) if (s.spiel[i][j]!= s.FREI){ switch (s.spiel[i][j]) { case s.WASSER: // Erzeuge blaues Quadrat screen.setColor(Color.blue); //i*20+1 bzw. 20-1=19 damit die Randung bleibt screen.fillRect(i*20+1,j*20+1,19,19); break; case s.SCHIFF: // Erzeuge gruenes Quadrat screen.setColor(Color.green); //i*20+1 bzw. 20-1=19 damit die Randung bleibt screen.fillRect(i*20+1,j*20+1,19,19); break; case s.VERSENKT: // Erzeuge rotes Quadrat screen.setColor(Color.red); //i*20+1 bzw. 20-1=19 damit die Randung bleibt screen.fillRect(i*20+1,j*20+1,19,19); 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.*; import java.awt.*; public class SpielCanvasPos1 extends SpielCanvas { SelectionAreaPos1 sa1; public SpielCanvasPos1 (Spielfeld s) { super(s); //Setze Layout so gross wie moeglich. setLayout(new GridLayout(1,0)); sa1 = new SelectionAreaPos1(s); add(sa1); validate(); } } 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.*; import java.awt.*; public class SpielCanvasPos2 extends SpielCanvas { public SpielCanvasPos2 (Spielfeld s, Quadrate[] q) { super(s); //Setze Layout so gross wie moeglich. setLayout(new GridLayout(1,0)); add(new SelectionAreaPos2(s, q)); validate(); } } 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.*; import java.awt.event.*; public class SelectionArea extends Canvas { Point tmp; Spielfeld s; public SelectionArea(Spielfeld s) { super(); this.s = s; tmp = new Point(-1,-1); validate(); } public void paint(Graphics screen) { int i,j; // Erzeuge Quadrat als Spielfeldbegrenzung screen.setColor(Color.white); screen.fillRect(0,0,200,200); screen.setColor(Color.black); screen.drawRect(0,0,200,200); // Erzeuge horizontale und vertikale Linien for (i = 1; i < 10; i++){ screen.drawLine(0, 0+i*20,200, 0+i*20); screen.drawLine(0+i*20, 0,0+i*20, 200); } // Markiere Schiffe und Wasser for (i = 0; i < 10; i++) for (j = 0; j < 10; j++) if (s.spiel[i][j]!= s.FREI){ switch (s.spiel[i][j]) { case s.WASSER: // Erzeuge blaues Quadrat screen.setColor(Color.blue); screen.fillRect(i*20+1,j*20+1,20-1,20-1); break; case s.SCHIFF: // Erzeuge gruenes Quadrat screen.setColor(Color.green); screen.fillRect(i*20+1,j*20+1,19,19); break; case s.VERSENKT: // Erzeuge rotes Quadrat screen.setColor(Color.red); screen.fillRect(i*20+1,j*20+1,19,19); break; default: break; } } //Zeichne Koordinate ein if (tmp.x != -1){ screen.setColor(Color.green); screen.fillRect(tmp.x*20+1,tmp.y*20+1,19,19); } } } 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.*; import java.awt.event.*; public class SelectionAreaPos1 extends SelectionArea{ MyListener myListener; public SelectionAreaPos1(Spielfeld s) { super(s); myListener = new MyListener(); addMouseListener(myListener); tmp.x=tmp.y=-1; } class MyListener extends MouseAdapter { 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.*; import java.awt.*; import java.awt.event.*; public class SelectionAreaPos2 extends SelectionArea { MyListener myListener; Quadrate[] q; public SelectionAreaPos2(Spielfeld s, Quadrate[] q) { super(s); myListener = new MyListener(); addMouseListener(myListener); this.q = q; } 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[]; public MyListener () { p = new Point[2]; p[0] = new Point(-1,-1); p[1] = new Point(-1,-1); } 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 (p[0].x==-1){ if ((s.spiel[tmp.x][tmp.y]== s.FREI)||(s.spiel[tmp.x][tmp.y]== s.SCHIFF)) { p[0].x = tmp.x; p[0].y = tmp.y; } else { //Falsche erste Position tmp.x = tmp.y = -1; } 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; p[1].y = tmp.y; p = drehePunkte(p); // Soll ein Schiff entfernt werden? if (s.spiel[p[0].x][p[0].y]==s.SCHIFF){ loescheSchiff(p); s.reorganisiereSpielfeld(); p[0].x = p[0].y = p[1].x = p[1].y = tmp.x = tmp.y= -1 ; return; } 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 if (checkPosition(p)){ //Schiff im Spielfeld markieren for (int i = 0; i<=abs(p[0].x-p[1].x);i++) for (int j = 0; j<=abs(p[0].y-p[1].y);j++){ if (p[0].x<p[1].x) //Horizontales Schiff s.spiel[p[0].x+i][p[0].y] = s.SCHIFF; else s.spiel[p[0].x][p[0].y+j] = s.SCHIFF; } s.reorganisiereSpielfeld(); repaint(); for (int i = 0; i<5;i++) 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 Object anker = getParent(); while (! (anker instanceof Frame)) anker = ((Component) anker).getParent(); FehlerFenster dialog = new FehlerFenster((Frame)anker, Falsche Positionsangabe!"); dialog.setVisible(true); dialog.setSize(200,150); p[0].x = p[0].y = p[1].x = p[1].y = tmp.x = tmp.y= -1 ; repaint(); } } } 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; int a = 0; if (abs(p[0].x-p[1].x)== 0) 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 i=0;i<abs(p[0].x-p[1].x)+1;i++) for (int j=0;j<abs(p[0].y-p[1].y)+1;j++) if (p[0].x!= p[1].x) //Horizontales Schiff test = test && (s.spiel[p[0].x+i][p[0].y]==s.FREI); else //Vertikales Schiff test = test && (s.spiel[p[0].x][p[0].y+j]==s.FREI); 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 a = abs(p[0].x-p[1].x); else //Vertikales Schiff a = abs(p[0].y-p[1].y); //Richte Schiff in Abhaengigkeit von dessen Laenge ein switch (a+1) { case 2: if (richteSchiffEin(0,p)==false) return false; break; case 3: if (s.schiff[1].p[0].x == -1){ //Erster Dreier if (richteSchiffEin(1,p)==false) return false; } else //Zweiter Dreier if (richteSchiffEin(2,p)==false) return false; break; case 4: 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; Point tmp = new Point(); if (s.schiff[zahl].p[0].x != -1) //Schiff bereits belegt return false; //Schiffskoordinaten setzen s.schiff[zahl].p[0].x = p[0].x; s.schiff[zahl].p[0].y = p[0].y; s.schiff[zahl].p[1].x = p[1].x; s.schiff[zahl].p[1].y = p[1].y; s.schiffZahl = s.schiffZahl - (p[1].x-p[0].x) - (p[1].y-[0].y)-1; return true; } 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; //Zu loeschendes Schiff finden while (schiffSuche(p,x) == false) x++; //Spielfeld freigeben for (int i=0; i<abs(s.schiff[x].p[0].x-s.schiff[x].p[1].x)+1; i++) 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 s.schiff[x].p[0].x=s.schiff[x].p[1].x=-1; s.schiff[x].p[0].y=s.schiff[x].p[1].y=-1; s.schiffZahl = s.schiffZahl + (p[1].x-p[0].x) + (p[1].y-p[0].y)+1; for (int i = 0; i<5;i++) 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 //gehoert 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(); if (p[0].x>p[1].x){ tmp.x = p[0].x; p[0].x=p[1].x; p[1].x=tmp.x; } if (p[0].y>p[1].y){ tmp.y = p[0].y; p[0].y=p[1].y; p[1].y=tmp.y; } return p; } protected int abs(int a){ 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.*; import java.awt.event.*; import java.applet.*; public class SchiffFenster extends Frame implements ActionListener{ SpielCanvas sc1; Spielfeld s; Quadrate q[]; 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 Panel centerPanel; // Spielfeld und Panel 2 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; this.c = c; s.initialisieren(); q = new Quadrate[5]; //Generelle Aufteilung des GUIs setLayout(new BorderLayout(0,0)); setSize(300,250); centerPanel = new Panel(); centerPanel.setLayout(new GridLayout(1,2,0,0)); centerPanel.setBounds(0,0,300,220); add("Center", centerPanel); 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 sc1 = new SpielCanvasPos2(s, q); 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 = new Panel(); 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.setLayout(new GridLayout(2,1,0,0)); 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(); panel1.setLayout(null); q[schiffNummer] = new Quadrate(s, schiffNummer); q[schiffNummer].setBounds(5,5,20,20); panel1.add(q[schiffNummer]); label3 = new Label(name); label3.setBounds(30,5,100,20); panel1.add(label3); // Anlegen der Buttons if (val==false){ Button fertig = new Button("Fertig"); fertig.setBounds(130,5,60,30); panel1.add(fertig); fertig.addActionListener(this); } panel2.add(panel1); } 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(); setVisible(false); s.schiffZahl = 17; s.spielfeldFertigstellen(); c.setSize(c.getSize().width+1, c.getSize().height+1); } else { Object anker = sc1.getParent(); while (! (anker instanceof Frame)) anker = ((Component) anker).getParent(); FehlerFenster dialog = new FehlerFenster((Frame)anker, "Erst alle Schiffe setzen!!"); dialog.setVisible(true); dialog.setSize(150,100); } } } 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.*; class Quadrate extends Canvas { int nummer; Spielfeld s; public Quadrate (Spielfeld s, int nummer) { super(); this.s=s; this.nummer = nummer; } public void paint(Graphics screen) { int i; screen.setColor(Color.black); // Erzeuge Quadrat als Spielfeldbegrenzung screen.drawRect(0,0,15,15); // Erzeuge Quadrat als Spielfeldbegrenzung if (s.schiff[nummer].p[0].x!=-1) screen.setColor(Color.green); else screen.setColor(Color.red); //Das eigentliche Quadrat muss die Koordinaten (1,1,14,14), // nicht aber (0,0,15,15) haben, damit die Umrandung sichtbar //ist screen.fillRect(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.*; import java.awt.event.*; public class FehlerFenster extends Dialog implements ActionListener{ public FehlerFenster(Frame dw, String title) { super(dw, title, false); this.setLayout(new BorderLayout()); Button b = new Button("Cancel"); b.addActionListener(this); add("Center", b); pack(); } public void actionPerformed(ActionEvent e) { 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