Schiffeversenken Extreme

Hallo,

mein Name ist Jonas und ich bin 17 Jahre alt. Mit Java habe ich schon seit längerer Zeit experimentiert, aber nie die Zeit und Lust für größere Projekte gefunden. Jetzt habe ich mich dazu entschlossen, ein Multiplayerspiel mit dem Titel “Schiffeversenken Extreme” zu programmieren. Wie der Name bereits sagt, wird sich das Spiel um den Klassiker Schiffeversenken drehen, aber mit einigen Änderungen. Die größte ist wahrscheinlich, dass es real-time basiert ist. Zwei Spieler können per Kommandozeile Befehle eingeben, die z.B. Schiffe steuern, andere Schiffe angreifen, usw. können. Im Moment arbeite ich noch am Grundgerüst des Spiels und habe noch keine (eigentlichen) Spielinhalte hinzugefügt, da meiner Meinung das Grundkonzept erstmal stimmen sollte, sodass das hinzufügen von Spielinhalten kein größeres Problem darstellt. Ich bin soweit, dass Befehle eingeben können und diese verarbeitet werden. Außerdem steht Kommunikation mit dem Server und sich zwei Clients können sich connecten. Wenn das Grundkonzept stimmt, will ich mich weiter an die Spielinhalte wagen.

Meine Bitte an euch Profis wäre es, über den Quellcode drüberzuschauen und mir Tipps und Verbesserungen zu Vorgehensweisen, Struktur und Implementierung zu geben :). Damit ihr euch besser zurechtfindet, werde ich die einzelnen Teile kurz erklären und aus meiner Sicht problematische Stellen erläutern (die auch anders hätten gelöst werden können).

Generell zur Struktur des Quellcodes will ich sagen, dass ich zum besseren Lernen und Verständnis auf keine Frameworks/Libarys (später maximal zum Einlesen der Map) zurückgreifen möchte. Außerdem lege ich auch Wert auf die Qualität des Codes und die saubere Implementierung. Für verschiedene Aufgaben habe ich versucht, Design Patterns zu verwenden (inwiefern mir das gelungen ist, erfahre ich hoffentlich von euch :D). Mir geht es weniger darum, einfach schnell etwas hinzuklatschen, sondern ich will mir dafür Zeit nehmen und dabei etwas lernen. Ich hab zum jetzigen Stand nur die wichtigsten Stelle kommentiert, trotzdem kann man sich schnell zurecht finden. Genug gebrabbelt, los gehts :p:

Package “main”:
In diesem Package befindet sich die Hauptklasse mit der main Methode. Diese initialsiert den Logger, welche in eine Datei schreibt (hab gelesen, es ist besser, für jede Klasse einen eigenen Logger zu haben -> Warum?). Danach wird der GameManager, welche den aktuelle GameState und User beinhaltet übergeben und die Input Loop startet (-> main Thread).

Package “commands”:
Ich habe probiert, das “Command-Pattern” zu implementieren und die “Factory method pattern” zur Erzeugung der Commands. Das Problem an dieser Stelle war, die Spielinhalte zentral zu machen, sodass die verschiedenen “Commands” darauf zugreifen können. Um das Problem zu lösen habe ich mich dafür entschieden, das “Singleton-Pattern” beim GameManager zu verwenden. Damit kann jede Klasse auf alles zugreifen (z.B. User und später die TileMap und Entitys). Ich bin mir nur nicht sicher, ob das wirklich OOP ist. Gibt es hier schönere Alternativen?

Package “gamestates”:
Hierzu gibt es nicht viel zu sagen, ich habe bis jetzt den Gamestate für den Start und die Verbindung zum Server. Die einzelnen Gamestates müssen die update()-Methode implementiert haben.

Package “network”:
Für die Kommunikation mit dem Server will ich ein “Message System” verwenden. Die Messages haben eine Methode zum Empfangen und Senden von Daten (ähnlich wie beim Command System werden die Messages durch eine Message Factory übergeben). Zum Verbindungsaufbau nutze ich die Klasse Session, welche einen weiteren Thread für den Networkinput erstellt. Dieser kann die Inputloop anhalten, wenn es erforderlich ist und natürlich auch wieder weiterlaufen lassen. Dafür werden die beiden Bytes (0 und -1) verwendet. Wenn die Verbindung zum Server beendet wird, schickt der Client zuerst ein Byte (127) an den Server. Dieser schickt zur Bestätigung das Byte zurück an den Client. Damit ist die Verbindung beendet. Wegen dieser drei Sonderfälle habe ich mich dazu entschieden, ein switch Konstrukt zu verwenden. Gibt zu der Kommunikation andere, schönere Varianten?

Soviel zum Client. Der Server ist bis jetzt sehr schlicht und kann zwei Verbindungen annehmen und die Verbindung im Falle einer Exception ordnungsgemäß beenden. Auch hier habe ich einen GameManager genutzt (“Singleton-Pattern”). Außerem habe ich angefangen das “Observer-Pattern” zu implementieren. Die beiden Player sind die Listener, die sich beim GameManager registrieren müssen.

Wenn ihr Fragen zu bestimmten Stellen im Quellcode habt, bitte fragen. Ich bedanke mich gaanz herzlich für eure Mühe :),
Jonas

Ein komplettes „Review“ wäre ein bißchen viel, aber ich hab’ mal den Code überflogen. Speziell zu Fragen der Form „Ist es sinnvoll, das so zu strukturieren?“ kann man auf dieser Basis aber nur schwer etwas sagen, weil man dafür den Überblick über das genaue Zusammenspiel der Klassen bräuchte.

Einen eigenen Logger pro Klasse, weil man dann die Logger hierarchisch Konfigurieren kann. Man kann z.B. für ein komplettes package das Logging abschalten, und für ein anderes das Logging auf „INFO“ setzen - bzw. eben dann auch Klassenweise. Dass der Pfad der Log-Datei hartverdrahtet ist, ist natürlich nicht gut, aber vielleicht erstmal nur ein Detail.

Hmja, Singleton… hitzige Flamewars und Diskussionen, bis hin zu Verteufelungen. Allgemein kann man sagen: Sinn des Singletons ist es NICHT, „praktisch“ zu sein, „weil man dann von überall auf die Sachen zugreifen kann“. Das ist NICHT Sinn und Zweck des Singletons. Das Singleton ist für die Fälle, wo es nur eine Instanz einer Klasse geben DARF. Das ist selten der Fall, aber man könnte sagen, dass speziell eine Socketverbindung für so ein Spiel so ein Fall sein könnte. Dann sollte das Singleton aber auch nur dafür zuständig sein, und nicht im o.g. Sinn den ganzen Game-State der ganzen Welt bekannt machen. Konkrete Hinweise, wie man das „besser“ machen könnte, sind aus den oben genannten Gründen schwierig. Sowas wie

interface Command {
    void executeOn(Game game);
}

kommt mir spontan in den Sinn, aber die Frage, WELCHE Commands genau WAS machen und welche Informationen sie dafür brauchen müßte man genauer analysieren.

Ganz allgemein wird da ja ein „Protokoll“ definiert. Auch da könnte man bei Planung und Abstraktion sehr weit gehen, und landet dann irgendwann bei GitHub - protocolbuffers/protobuf: Protocol Buffers - Google's data interchange format … Bei einem einzelnen Spiel, bei dem man davon ausgeht, dass die Mange der Messages beschränkt (und das Spiel irgendwann einfach „fertig“ ist, und garantiert nichts mehr geändert oder erweitert wird) tut es aber wohl auch eine spezielle, eigene Lösung.

Hab weder grad viel Zeit (morgen ZP :D), noch bin ich Profi, aber mal so rein generell:

Ich würde allen empfehlen die Implementierung komplett in Englisch zu halten. Wieso, das sollte sich
jeder selbst erschließen können, (kann man auch viel zu diskutieren, aber naja ^^), aber selbst wenn
man sich für deutsch entscheidet, dann auf keinen fall die sprachen mischen. Das ist seeehr sehr gräßlich :smiley: (wie wird das geschrieben o.o)

@Marco13
Ich danke dir für deine Tipps und das du einmal grob über den Code geschaut hast.

Ganz allgemein wird da ja ein „Protokoll“ definiert. Auch da könnte man bei Planung und Abstraktion sehr weit gehen, und landet dann irgendwann bei GitHub - protocolbuffers/protobuf: Protocol Buffers - Google's data interchange format … Bei einem einzelnen Spiel, bei dem man davon ausgeht, dass die Mange der Messages beschränkt (und das Spiel irgendwann einfach „fertig“ ist, und garantiert nichts mehr geändert oder erweitert wird) tut es aber wohl auch eine spezielle, eigene Lösung.

Kann man das message System nicht als „Protokoll“ betrachten? Es wird ja auch hier festgelegt, welche Daten eingelesen und welche ausgelesen wird. Oder liegt es an meiner Implementierung (wobei ich festgestellt habe, dass ich da auch noch nicht zuende gedacht habe und es da noch einige Bugs gibt ^^)?

@mymaksimus
Eig. wollte ich auch alles in Englisch halten :D.

Ja, sicher, das MessageSystem IST ein Protokoll. Allerdings ein sehr individuelles. Ich wollte nur andeuten, dass man da sehr weit verallgemeinern kann und so, so dass man seine Messages später in einer Datei definieren und dann nur noch den Codegenerator laufen lassen müßte. Je nachdem, wie aufwädnig die Kommunikation (das Protokoll) wird, und wie lange es gewartet und ggf. erweitert werden muss, könnte man sich überlegen, wie man das Protokoll an sich formal definieren könnte. Aber das ist hier natürlich nicht “nötig”.

@Marco13 : Also für die Idee mit den Commands hat sich in Zeiten der VarArgs und Generics schon folgendes bewährt:

  T execute(Object... args);
  VariableScope getScope(Object accessor);
}```Simple Commands hängen so nichtmal an der Engine mit der Klasse "Game" fest. Ohne Generics geht das ganze sogar auch und möglicherweise sogar besser. Scripte gestalten sich dann als "List<Command>" oder als "Map<Label, Command>". Dazu noch "VariableScopes"s und fertig ist die Script-Engine. ;)

Bei Schiffe versenken braucht man sowas aber kaum. Da genügt es, zwei Koordinaten zu übermitteln und eine Antwort WASSER, TREFFER oder VERSENKT abzuwarten.

Was das Netzwerk angeht, so wäre in diesem Fall ja eine Voll-Duplex-Verbindung (Server-Client auf beiden Seiten) angemessen.

@Spacerat : “Voll-Duplex-Verbindung (Server-Client auf beiden Seiten)”

huch? könntest du das genauer erklären?

Was ist denn eine Server-Client-Verbindung? Ein Client sendet Anfragen an einen Server und bekommt dafür eine Reaktion. Andersherum sendet ein Server niemals Anfragen an den Clienten. Mit Voll-Duplex meine ich in diesem Fall Server-Client-Verhalten auf beiden Seiten. Voll-Duplex sollte man hier aber nicht auf die Goldwaage legen, der FB gehört eigentlich in eine andere Sparte. :wink: