communityWir suchen ständig neue Tutorials und Artikel! Habt ihr selbst schonmal einen Artikel verfasst und seid bereit dieses Wissen mit der Community zu teilen? Oder würdet ihr gerne einmal über ein Thema schreiben das euch besonders auf dem Herzen liegt? Dann habt ihr nun die Gelegenheit eure Arbeit zu veröffentlichen und den Ruhm dafür zu ernten. Schreibt uns einfach eine Nachricht mit dem Betreff „Community Articles“ und helft mit das Angebot an guten Artikeln zu vergrößern. Als Autor werdet ihr für den internen Bereich freigeschaltet und könnt dort eurer literarischen Ader freien Lauf lassen.

Battleship - Beginn des Projekts Drucken E-Mail
Benutzerbewertung: / 87
SchwachPerfekt 
Sonntag, den 10. Mai 2009 um 00:00 Uhr
Beitragsseiten
Battleship
Beginn des Projekts
Model
Netzwerkprogrammierung in Java
Netzwerkprotokolle
Routing
View
Computerspieler
Controller
Alle Seiten

Beginn des Projekts

Im letzten Kapitel haben wir einige wichtige Informationen zusammengefasst, die einem dabei helfen eine Software zu planen, zu entwerfen und zu bauen. Unser Projekt soll das Spiel »Schiffe versenken« werden. Die Zielbestimmung im Pflichtenheft beschreibt nach einer exzessiven Brainstorming-Runde eine Applikation mit den folgenden Anforderungen.

Anforderungen

Die genannten Anforderungen für das Spiel »Schiffe versenken« sind grob, lassen aber eine erste Entwicklungsrichtung erkennen. Wir beginnen damit eine triviale Struktur auszuarbeiten. Die Checkliste für die zentralen Bautechniken eignet sich hierfür besonders gut.

Wir legen zunächst einmal fest in welchem Ausmaß der Entwurf im Vorfeld erstellt und in welchem Maß er beim Schreiben des Codes - an der Tastatur - entwickelt wird. Da es sich um ein relativ kleines Open-Source Projekt handelt, werden wir die iterative Vorgehensweise wählen und uns nicht allzu lange mit dem konzeptionellen Entwurf aufhalten. Neben dem Entwurf des Codes mithilfe von Klassendiagrammen in der Unified Modeling Language, kurz UML, ist die Festlegung der Programmierkonventionen von großer Bedeutung. Zu den Schlüsselentscheidungen für den Bau zählt die Wahl der Programmiersprache. Eines der Anforderungen war die Tatsache, dass unser Spiel auf verschiedenen Plattformen lauffähig sein soll. Aus diesem Grunde werden wir auf die Programmiersprache Java zurückgreifen.

Java ist eine objektorientierte Programmiersprache, entwickelt von Sun Microsystems in den Neunzigern. Java-Programme werden in Bytecode übersetzt und dann in einer speziellen Umgebung ausgeführt, die als Java-Laufzeitumgebung oder Java-Plattform bezeichnet wird. Diese Umgebung steht für viele Plattformen zur Verfügung, so dass Java-Programme problemlos auf vielen Plattformen ausgeführt werden können. Java bietet viele Vorteile bei der Anwendungsentwicklung, sie hat eine automatische Speicher- und Heap-Verwaltung, ist robust, unterstützt Multi-Threading, ist einfach - weil strukturiert und stellt standardmäßig eine große Klassenbibliothek, samt einheitlichen Grafikfunktionen für verschiedene Windows-Systeme, zur Verfügung.

Unter dem Namen »Code Conventions for the JavaTM Programming Language« wurden von Sun Microsystems Richtlinien für die Programmierung mit Java veröffentlicht. Wir werden uns in diesem Projekt an diese Richtlinien halten und eins zu eins umsetzen.

Die Planung der Architektur

Die Planung der Architektur muss wohl durchdacht erfolgen. Der Grundaufbau, also das Fundament der Anwendung, kann später kaum mehr verändert werden, so dass schlecht entworfene Systeme anschließend nicht mehr korrigiert werden können.

Architekturmuster

Im Bereich der Softwareentwicklung sind Architekturmuster in den Arten von Mustern auf oberster Ebene einzuordnen. Im Gegensatz zu Idiomen oder Entwurfsmustern bestimmen sie nicht ein konkretes (meist kleines oder lokales) Teilproblem, sondern den Grundaufbau, das Fundament der Anwendung.

Umfangreiche Software von Grund auf neu zu entwickeln findet heutzutage nur noch selten statt. Dies wäre im Zusammenhang mit der heutigen Schnelllebigkeit und den ständig wachsenden Anforderungen nicht mehr wirtschaftlich. Daher werden Muster und ihre Beschreibungen eingesetzt, um ein erprobtes Lösungsschema als Vorgabe zu nutzen und entlang dieses Rahmens entwickeln zu können. Muster bestehen aus Beschreibungen von Problem-Lösungspaaren, welche über die Zeit von Experten zusammengetragen worden sind und ermöglichen so auch Neueinsteigern ohne langjährige Erfahrungen im Softwareentwicklungsbereich, häufig auftretende Entwurfsprobleme schneller zu lösen. Um komplexe Softwaresysteme entwickeln zu können sind Erfahrungswerte notwendig. Diese lassen sich gerade von Neulingen nicht im Hand umdrehen erlernen, sondern nehmen viel Zeit in Anspruch. Die allgemein zugänglichen Muster stellen einen mentalen Baustein dar, der diese Erfahrungslücke schließen kann.

Softwarearchitektur entscheidet mit über die Qualität des Systems. Eine gute Architektur ist gekennzeichnet durch Flexibilität. Gerade in der heutigen Zeit spielt Anpassbarkeit an ständige Änderungen und neue Ansprüche eine große Rolle. So kann es in einem bestimmten Kontext beispielsweise sinnvoll sein, ein großes System in mehrere, voneinander unabhängige Komponenten zu zerlegen um die Wartbarkeit der einzelnen Komponenten möglichst zur Laufzeit gewährleisten zu können, ohne alle Schnittstellen der zu wartenden Komponente beachten zu müssen. Die Wartbar- bzw. Anpassbarkeit des Systems würde in diesem Fall die „Kraft“ oder auch gewünschte Eigenschaft des Systems darstellen. Häufig stehen Muster nicht isoliert voneinander dar, sondern treten gemeinsam auf, da beispielsweise die Anwendung eines bestimmten Musters zu neuen Problemen führen kann, denen mit der Beschreibung anderer Muster begegnet wird. Desweiteren kann der Grund für die Anwendung von mehreren Mustern für einen Kontext darin liegen, dass ein Muster nicht alle „Kräfte“/ Anforderungen zufrieden stellend ausgleichen kann, sondern nur einige. Ein anderes Muster kann dann eingesetzt werden, um die unberücksichtigten Kräfte zu behandeln.

Im Zusammenhang mit der Softwareentwicklung von komplexen Systemen werden von den Entwicklern häufig Architekturmuster zur Komplexitätsbewältigung eingesetzt. Sie zeigen ein erprobtes Lösungsschema auf und beschreiben die beteiligten Komponenten mit ihren Beziehungen und Zuständigkeiten.

Während „Idiome“ Muster auf Programmiersprachenebene liefern, befassen sich „Entwurfsmuster“ mit der Verfeinerung von Subsystemen und Architekturmuster schließlich mit der Strukturierung von diesen. Die Muster befinden sich somit auf drei verschiedenen Abstraktionsebenen, wobei die Architekturmuster auf der höchsten Abstraktionsebene liegen. Sie liefern die grundsätzliche Strukturierung von Softwaresystemen und definieren des weiteren Subsysteme, deren jeweilige Zuständigkeiten, sowie ihre Interaktionen mit Hilfe von Regeln.

Model View Controller

Ein wichtiger Aspekt der modernen Softwareentwicklung ist das sogenannte Model-View-Controller Schema, ein bedeutendes Architekturmuster welches die wesentlichen Aufgabengebiete in seperate und miteinander in Wechselwirkung stehende Komponenten unterteilt. Nämlich das Model selbst, die Ansicht und den Controller. Während sich der Controller um die Interaktion mit dem Benutzer kümmert, liefert das Model z.B. Daten und teilt der Ansicht eventuelle Änderungen im Abauf mit. Die Ansicht kümmert sich um die Darstellung der Daten.

Wir werden in diesem Projekt auf das bewährte Architekturmuster MVC zurückgreifen. Vielmehr soll MVC den Eckpfeiler und das Fundament der gesamten Applikation bilden. Unser Spiel wird darauf aufbauen und alle Facetten dieses Muster ausreizen.

Falls Sie sich bisher mit MVC noch nicht beschäftigt haben oder Ihnen gewisse Teilaspekte ein Mysterium geblieben sind, werden wir diese in den nachfolgenden Kapiteln auflösen.

Bei MVC handelt es sich prinzipiell um ein leistungsfähiges zusammengesetztes Architekturmuster. Es erspart Ihnen viel Code-Schreiberei und trennt die teile Ihrer Applikation in logische Einheiten auf. Kurzum, sie reduziert die Komplexität, ein wichtiges Merkmal eines guten Entwurfs.
Um MVC zu verstehen, muss man die Entwurfsmuster verstehen, die das MVC ausmachen. Dazu zählt insbesondere das Observer Entwurfsmuster, das Strategy Entwurfsmuster und das Composite Entwurfsmuster.

MVC-Musterbrille

Das Model enthält die darzustellenden Daten und gegebenenfalls auch die Geschäftslogik. Es ist von Präsentation und Steuerung unabhängig. Die Bekanntgabe von Änderungen an relevanten Daten im Model geschieht nach dem Entwurfsmuster „Beobachter“. Das Model ist das zu beobachtende Subjekt, auch Publisher, also „Veröffentlicher“, genannt. Es kennt die Views nicht und agiert vollständig autonom.

Die View ist die Präsentationsschicht und für die Darstellung der benötigten Daten aus dem Model, sowie für die Entgegennahme von Benutzerinteraktionen zuständig. Es handelt sich praktisch um Ihre Sicht auf das Model. Sie kennt sowohl ihre Steuerung als auch das Model, dessen Daten sie präsentiert, ist aber nicht für die Weiterverarbeitung der vom Benutzer übergebenen Daten zuständig.
Im Regelfall wird die Präsentation über Änderungen von Daten im Model mithilfe des Entwurfsmusters „Beobachter“ unterrichtet und kann sich daraufhin die aktualisierten Daten besorgen. Die Präsentation verwendet das Entwurfsmuster „Kompositum“.

Der Controller verwaltet eine oder mehrere Präsentationen, nimmt von ihnen Benutzeraktionen entgegen, wertet diese aus und agiert entsprechend. Es ist nicht die Aufgabe der Steuerung, Daten zu manipulieren. Die Steuerung entscheidet aufgrund der Benutzeraktion in der Präsentation, welche Daten im Model geändert werden müssen. Sie enthält weiterhin Mechanismen, um die Benutzerinteraktionen der Präsentation einzuschränken.

Präsentation und Steuerung verwenden zusammen das Entwurfsmuster „Strategie“, wobei die Steuerung der Strategie entspricht. Der Controller kann in manchen Implementierungen ebenfalls zu einem „Beobachter“ des Models werden, um bei Änderungen der Daten die View direkt zu manipulieren.

Was ist die eigentliche Aufgabe des Controllers? Die View könnte theoretisch auch die Benutzereingaben vollständig verwalten und an das Model weiterleiten. MVC separiert die Aufgabenbereiche und hält die View dadurch sauber, gleichzeitig entkoppelt der Controller die View vom Model. Auf diese Weise lässt sich das Model leicht mit einer anderen View verwenden. Der Controller trennt die Steuerungslogik vom View und gestaltet das Design dadurch flexibel, so dass es später leicht erweitert werden kann.

Das Grundgerüst

Eine wesentliche Entscheidung wurde nun getroffen. Mit dem Model View Controller haben wir uns für ein leistungsstarkes Architekturmuster für unser Spiel entschieden.

Wir werden nun ein erstes Klassendiagramm entwerfen. Das Model repräsentiert die Daten des Spieles »Battleship«. Zu einem Spiel gehören zwei Spieler, die abwechselnd auf Koordinaten feuern. Die Spielfelder bestehen aus x×y Kästchen, die man an den Seiten und an den oberen Rändern mit Zahlen versieht. Diese stellen einmal das eigene und dann das gegnerische Meer oder Kampfgebiet dar. In das eigene Meer trägt man nun, ohne dass der Mitspieler dies sieht, seine Flotte ein. Dies geschieht, indem man Gebilde von unterschiedlicher Kästchenlänge einzeichnet. Über die Anzahl und Größe der Schiffe und über die Art der Platzierung sollten vor Spielbeginn Einigkeit herrschen.

Folgende Spielregeln müssen eingehalten werden:

  • Die Schiffe dürfen nicht aneinander stoßen.
  • Die Schiffe dürfen nicht über Eck gebaut sein oder Ausbuchtungen besitzen.
  • Die Schiffe dürfen auch am Rand liegen.
  • Die Schiffe dürfen nicht diagonal aufgestellt werden.
  • Jeder verfügt über insgesamt neun Schiffe, abhängig von der Spielfeldgröße:
    • Ein Battleship (5 Kästchen)
    • Zwei Cruiser (je 4 K.)
    • Ein Tankship (3 Kästchen)
    • Zwei Minesweeper (je 2 K.)
    • Drei Speedboat (je 1 K.)

Die Positionen der Schiffe werden zum Spielanfang durch die Spieler oder automatisch durch den Computer bestimmt.

Schiffeversenken

Die Definition der Daten im Model

Das Model sollte mehrere Datentypen verwalten können. Neben den Spielern, muss es pro Spieler mindestens einen Datensatz von jeweils zwei Spielfeldern verwalten. Ein Spielfeld besteht aus Feldern, die unterschiedliche Zustände einnehmen können. Wir definieren daher zunächst eine separate Klasse vom Typ enum, die die möglichen Zustände eines Feldes definiert.

public enum FieldState implements Serializable
{
    /** Marks a destroyed ship */
    DESTROYED_SHIP,
 
    /** Marks a field which was hit */
    HIT, 
 
    /** Marks a field */
    MARKED,
 
    /** Marks a field which has a ship */
    SHIP,
 
    /** Marks an unknown field */
    UNKNOWN, 
 
    /** Marks a water field */
    WATER;
 
    /**
     * Determines if a de-serialized file is compatible with this class.
     *
     * Maintainers must change this value if and only if the new version
     * of this class is not compatible with old versions. See Sun docs
     * for <a href=http://java.sun.com/products/jdk/1.1/docs/guide
     * /serialization/spec/version.doc.html> details. </a>
     *
     * Not necessary to include in first version of the class, but
     * included here as a reminder of its importance.
     */
    private static final long serialVersionUID = 3103200979172470511L;
}

Wie Sie vielleicht auf den ersten Blick erkennen können, implementieren wir das Interface Serializable. Dies wird es uns später ermöglichen das vollständige Model mitsamt seinen Daten als Objekt zu serialisieren um es anschließend persistent abzuspeichern. Dazu wird der komplette Zustand des Objektes, inklusive aller referenzierten Objekte, in einen Datenstrom umgewandelt, der anschließend auf ein Speichermedium geschrieben wird.

Die Schiffe

Es existieren unterschiedliche Arten von Schiffstypen. Unser Spiel soll mit den folgenden Typen auf einem 10x10 Feld arbeiten:

Typ Engl. Bezeichnung Länge Breite Anzahl
Flugzeugträger Aircraftcarrier 6 1 0
Schlachtschiff Battleship 5 1 1
Kreuzer Cruiser 4 1 2
Tanker Tankship 3 1 1
Minensuchboot Minesweeper 2 1 2
Schnellboot Speedboat 1 1 3

Alle Schiffe haben bestimmte Eigenschaften, weshalb sich an dieser Stelle die Generalisierung aufdrängt. Eine Generalisierung in der UML ist eine gerichtete Beziehung zwischen einem generelleren und einem spezielleren Classifier.

Alle Schiffe erweitern die abstrakte Klasse Ship.

public abstract class Ship implements Serializable, Cloneable
{
    /**
     * Default Constructor.
     * 
     * @param type  the ship type
     * @param width the width of the ship
     * @param length    the length of the ship
     */
    public Ship(String type, int width, int length) 
    {
        this(type, width, length, 0, 0);
    }
 
    /**
     * Inits a new ship.
     * 
     * @param type  the ship type
     * @param width the width of the ship
     * @param length    the length of the ship
     * @param score the score for destroying this ship
     * @param damage    an initial damage
     */
    public Ship(String type, int width, int length, int score, int damage) 
    {
        type_ = type;
        length_ = length;
        width_ = width;
        score_ = score;
        damage_ = damage;
        coordinates_ = new ArrayList<Coordinate>();
    } 
 
    /**
     * Allows to clone an object.
     * 
     * @return  an object copy
     * @throws java.lang.CloneNotSupportedException
     */
    @Override
    public Object clone() throws CloneNotSupportedException 
    {
        // Get initial bit-by-bit copy, which handles all immutable fields
        return super.clone();
    }
 
    /**
     * Get the ship length.
     * 
     * @return  the length
     */
    public int getLength()
    {
        return length_;
    }
 
    /**
     * Get the ship width.
     * 
     * @return  the width
     */
    public int getWidth()
    {
        return width_;
    }
 
    // Rest siehe Quellcode...
}

Ein wesentlicher Teil der Rahmendaten wurde nun definiert. Bevor wir uns den Spielfelder widmen generieren wir eine weitere Klasse, die sich später als nützlich erweisen wird. Jeder Spieler besitzt einen Satz von Schiffen. Diesen Satz kann man unter einer Schiffsflotte vereinen.

public final class Fleet implements Serializable, Cloneable
{
    /**
     * Standard-Constructor.
     */
    public Fleet()
    {
        fleet_ = new ArrayList<Ship>();
    }
 
    /**
     * Constructs a new fleet, with a given fleet list. Since a fleet array
     * consists of ship objects, this method will just add a shallow reference
     * to the old fleet array. 
     * 
     * If you want to copy a whole fleet, use clone().
     * 
     * @param fleet the fleet
     */
    public Fleet(ArrayList<Ship> fleet)
    {
        fleet_ = fleet;
    }  
 
    /**
     * Implement clone as follows:
     * <ul>
     * <li>the class declaration "implements Cloneable" (not needed if already
     * declared in superclass)
     * <li>declare clone method as public
     * <li>if the class is final, clone does not need to throw CloneNotSupportedException
     * <li>call super.clone and cast to this class
     * <li>as in defensive copying, ensure each mutable field has an independent copy
     * constructed, to avoid sharing internal state between objects
     * </ul>
     * 
     * @return  an object copy
     * @throws java.lang.CloneNotSupportedException
     */
    @Override
    public Object clone() throws CloneNotSupportedException 
    {
        // Get initial bit-by-bit copy, which handles all immutable fields
        Fleet result = (Fleet)super.clone();
 
        // Mutable fields need to be made independent of this object, for reasons
        // similar to those for defensive copies - to prevent unwanted access to
        // this object's internal state. Strings are always immutable.
        result.fleet_ = getShipArrayCopy();
 
        return result;
    }   
 
    /**
     * Returns a *deep* copy of the ship array.
     * 
     * @return  a ship array list
     */
    public ArrayList<Ship> getShipArrayCopy()
    { 
        ArrayList<Ship> fleet = new ArrayList<Ship>();  
 
        try {
            // We need to copy and add every single ship object
            for(Ship s : fleet_) {
                fleet.add( (Ship)s.clone() );
            }
            return fleet;
        } catch(CloneNotSupportedException e){            
        }
 
        return null;
    }   
 
     /**
     * Return the fleet ship array.
     *
     * @return  a ship array list
     */
    public ArrayList<Ship> getShipArray()
    {
        return fleet_;
    }
 
    /**
     * Appends the specified ship to the end of this fleet.
     * 
     * @param ship  ship to be appended to this list 
     * @return  true (as per the general contract of Collection.add)
     */
    public boolean addShip(Ship ship)
    {
        if( ship == null )
            return false;
 
        return fleet_.add(ship);
    }
 
    // Rest siehe Quellcode...
}
Die Spielfelder

Wir haben bereits festgestellt, dass zu einem Spieler zwei Spielfelder gehören. Das eigene Spielfeld mit der Draufsicht auf die eigenen Schiffe, sowie das Spielfeld des Gegners, dessen positionierte Schiffe wir nicht sehen können. Wir wählen einen Ansatz mit zwei unterschiedlichen Klassen, die jeweils die abstrakte Klasse Board.java erweitern, nämlich HomeBoard.java und EnemyBoard.java.

Die Klasse EnemyBoard.java beinhaltet kaum Funktionalitäten mit Ausnahme von Feldanzeigen, da wir auf dem Spielfeld des Gegners keine nennenswerten Operation ausführen können. Die Klasse HomeBoard.java hingegen implementiert diverse Methoden, darunter die Methode removeShips(), die alle Schiffe von einem Spielfeld entfernt.

/**
 * Removes all ships from a game board. This is an internal method
 * to clean a board completely from ships.
 * 
 * @param state the state to set
 */
public void removeShips(FieldState state)
{
    Field f = null;
 
    // Call all fields and release them from ships.
    for(int row = 0; row < height_; row++) {
        for(int column = 0; column < width_; column++) {                
            f = getField( column, row );
            if(f != null) {
                // Delete the ship from the field, if there is one
                if( f.hasShip() ) {
                    // First delete coords of the ship
                    f.getShip().clearCoords();
                    // Now remove it from the field
                    f.deleteShip( state );
                }
            }
        }   
    }
}

Zur Lokalisierung eines Feldes wird eine seperate Klasse namens Coordinate.java verwendet, welche Koordinatenpunkte in einem zweidimensionalen Raum speichert.

Die beiden genannten Spielfelder werden abschließend zu einem Set verknüpft. Dieses Set nennen wir BoardSet.java. Jeder Spieler besitzt genau einen solchen Satz.

Die Spieler

Neben den Spielfeldern muss das Model auch die Daten eines Spielers verwalten. Ein Spieler trägt eine eindeutige Identifikationsnummer (Id) und kann weitere Personenmerkmale tragen. Dazu zählt der Name, der Geburtstag, das Geschlecht der Person und auch die Zahl der abgegebenen Schüsse. Weitere Merkmale sollten zu einem späteren Zeitpunkt definiert werden können.

Die Klasse PlayerInfo.java beinhaltet alle wesentlichen Datensätze zu einer Person.

public class PlayerInfo implements Serializable
{
    /**
     * Constructor. Creates an empty player profile, with an id.
     * 
     * @param id  the player id
     */
    public PlayerInfo(String id)
    {
        this( id, "" );
    }
 
    /**
     * Constructor. Inits all variables, and creates a new player profile.
     * 
     * @param id    the player id
     * @param name  the player name
     */
    public PlayerInfo(String id, String name)
    {
        id_ = id;
        name_ = name;
        idHash_ = Cryptography.getSha256( id.getBytes() );
    }
 
    /**
     * Set the players name.
     * 
     * @param name  the new name
     */
    public void setName(String name)
    {
        name_ = name;
    }
 
    // Rest siehe Quellcode...
}

Die Identifikationsnummer ist von entscheidender Bedeutung. Sie identifiziert einen Spieler im Model und erlaubt es ihm, auf Ressourcen zuzugreifen. Verliert ein Spieler seine Identifikationsnummer, so ist er anschließend nicht mehr in der Lage seine Daten im Model abzufragen. Gleichzeitig verhindert die Nummer den unerlaubten Zugriff auf fremde Daten im Model. So kann eine Person die Daten einer anderen Person nicht abfragen, wodurch das Geheimnisprinzip gewahrt bleibt.
Die Identifikationsnummer verhält sich analog zur neunstelligen »Social Security Number« in den Vereinigten Staaten, wo sie die Funktion eines allgemeinen Personenkennzeichens erfüllt und unter anderem von diversen Bundesbehörden genutzt wird. In den USA existiert eine Person ohne diese Nummer nicht und so verhält es sich auch in Battleship.



Zuletzt aktualisiert am Montag, den 23. April 2012 um 12:59 Uhr
 
AUSWAHLMENÜ