HARALD MELCHER

Flattiverse - Raumschiffe Programmieren in C#

Flimmerfrei anzeigen

Wenn das Gui Sonnen und andere Körper zeichnet, löscht es zuerst den Schirm und zeichnet dann alle Körper neu. Das flimmert, weil sich das Bild immer von leer zu gefüllt neu aufbaut.

Eine Abhilfe ist das Doppel-Puffern: Man zeichnet in einen Buffer im Hintergrund, der nicht angezeigt wird. Wenn das Bild im Hintergrund fertig ist, kopiert man es mit einer einzigen Operation auf den Vordergrund und ersetzt so das bisherige Bild. Da sich die meisten Körper nicht bewegt haben, flimmern sie dadurch nicht. Körper, die sich bewegt haben, erscheinen an der neuen Position.

Gui.cs

Der Konstruktor von Gui erstellt eine Bitmap bitmap in der benötigten Größe (Width und Height) und fügt folgenden Handler an das Load-Event und an das Resize-Event von Gui an:

private void Gui_Load(object sender, EventArgs e) {
    bitmap?.Dispose();
    bitmap = new Bitmap(Width, Height);
}

Dann setzt er im Konstruktor folgende Parameter:

SetStyle(ControlStyles.UserPaint, true);
SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
SetStyle(ControlStyles.AllPaintingInWmPaint, true);

Die OnPaint-Methode verwendet dann statt des Graphics-Kontextex aus e.Graphics den Graphics-Kontext der Bitmap mittels

Graphics g = Graphics.FromImage(bitmap)

Alle anderen Zeichenbefehle bleiben gleich.

Nach dem letzten Zeichenbefehl erfolgt noch das “Blitten”, dh. Kopieren der Bitmap auf den eigentlichen Schirm, dessen Graphics-Kontext noch in e liegt:

e.Graphics.DrawImage(bitmap, 0, 0);
Flattiverse - Raumschiffe Programmieren in C#

Anzeigen

Gui.cs

In der Gui-Klasse nimmt die Methode DisplayUnits(List<Unit> units) die Liste entgegen und erstellt eine lokale Kopie von der Liste. Danach ruft sie Invalidate() auf, damit Windows bei nächster Gelegenheit die Gui-Form neu zeichnet. Wie Zeichnen geht, erfährt die Form, indem wir ihre Methode OnPaint überschreiben.

OnPaint(PaintEventArgs e) erhält schon eine Graphics-Komponente, mit der man zeichnen kann: Graphics g = e.Graphics;.

Nun kann man durch die Liste der zu zeichnenden Units gehen und für jede Unit mit g.DrawEllipse() einen Kreis zeichnen.

Achtung: g.DrawEllipse() zeichnet den Kreis vom linken oberen Rand des einschließenden Quadrats, nicht vom Mittelpunkt!

Flattiverse - Raumschiffe Programmieren in C#

Umschauen

Body.cs

Scannen erfolgt von einem Schiff aus. Der Aufruf von NewShip erzeugt ein Schiff im angegebenen Universum.

await server.Player.Universe.NewShip("SuperShip")

Wenn das Schiff im Universum ist, kann es vom Schiff aus sichtbare Körper im Weltall sehen. Dazu hängt man einen Handler an server.ScanEvent, den der Connector immer aufruft, wenn neue Körper sichtbar sind.

Ab jetzt erfolgen immer dann Aufrufe des Handlers (s.o.), wenn das Schiff neue Körper sieht.

Die Handler-Methode erhält einen Flattiverse-ScanEvent, der über gescannte Körper informiert:

private void scanEvent(FlattiverseEvent @event) { ... }

Zur Anzeige neu gescannter Köper dient die von FlattiverseEvent abgeleitete Klasse NewUnitEvent. Eine einfache if-Anweisung filtert (zunächst) nur diese aus und ein Cast auf NewUnitEvent stellt sicher, dass wir auf dessen Eigenschaft Unit zugreifen können. Das ist dann ein Körper, der in der Nähe des Schiffes fliegt. Unit hat dabei z.B. den Typ Sun, Planet oder Target.

Die Body-Klasse kann diese Körper in einer Liste units sammeln und, wenn sie sich geändert hat, der Gui-Klasse (deren Referenz sie durch ConnectGui hat) durch Aufruf der Methode gui.DisplayUnits(units) zum Anzeigen geben.

Eine Herausforderung ist, dass die Körper direkt um’s Schiff herum schon NewUnitEvents triggern, während body.Start() noch läuft und das Gui möglicherweise noch nicht existiert. Das heißt, bei der Zuweisung des Gui zum Body ist die Liste schon gefüllt und es gibt momentan keine neuen Körper, die Events triggern.

Deshalb ruft ConnectGui die Methode gui.DisplayUnits(units) auf, damit die Anzeige auch die vor Erstellen des Guis bereits bekannten Körper kennt.

Flattiverse - Raumschiffe Programmieren in C#

Verbindung zum Server

Änderungen im Programm-Gerüst

Program.cs

Die von Visual Studio erstellte Programm-Klasse erzeugt im Aufruf von Application.Run(new Form1()) die Form mit der Benutzeroberfläche. Hier kann man Oberfläche und “Mechanik” des Raumschiffs trennen.

Zunächst erhält die Form-Klasse einen anderen Namen als Form1 (bei mir Gui), damit sie ihrer Bedeutung als Graphical User Interface gerechter wird. Beim Umbenennen zickt Visual Studio möglicherweise, so dass das eine Aufgabe für den Datei-Explorer wird.

Die Instanziierung der Gui-Klasse erfolgt in einem getrennten Schritt vor dem ganzen Application-Code und Application.Run(gui), so dass dazwischen noch Erstellen und Starten des Raumschiffes passt.

Der Raumschiff-Rumpf (bei mir Body) erfährt nach Erstellung im Application-Teil das Gui durch ConnectGui(gui), damit er darauf zugreifen kann.

Beim Starten des Rumpfs verwenden wir das await-Schhlüsselwort, weil das die erste eigene asynchrone Methode wird. Das hat zur Folge, dass die Main-Methode nun auch den Wert async Task haben muss.

Der Raumschiff-Körper

Body.cs

Die Start-Methode ist vom Typ async Task und erstellt zuerst eine neue Instanz des Server-Objekts aus der Connector-DLL. Über sie läuft die Kommunikation mit dem Server.

await server.Login(username, password) loggt die Spielerin oder den Spieler ein.

Welche Universen der Server kennt, liefert seine Universes-Property. Sie kennt auch die Galaxien zu jedem Universum.

In ein Universum kommt man mit Join, das eine Teamfarbe des jeweiligen Universums als Parameter hat:

Universe universe 
    = server.Universes["Beginners Course"]; 

await universe
    .Join(universe.Teams["None"]);

Damit kann unser Programm jetzt

  • sich mit dem Flattiverse-Server verbinden
  • sich mit Userid und Passwort anmelden
  • sich die Liste der Universen holen
  • in jedem Universum die Galaxien sehen
  • die passenden Teamfarben pro Universum holen

Flattiverse - Raumschiffe Programmieren in C#

Vorbereiten der Entwicklungs-Umgebung

Der Beispielcode hier ist mit Microsoft Visual Studio erstellt, das in verschiedenen Lizenzmodellen – auch frei – erhältlich ist.

Im Visual Studio das Projekt als Windows Forms App (.NET Core) anlegen. Der .NET Core Code läuft im allgemeinen schneller und der Code der Bibliotheken ist frei und Open Source. Im Moment hat Visual Studio allerdings für .NET Core noch keinen Form-Designer, so dass man entweder Forms “von Hand” erstellt oder eben doch ein Windows Forms App (.NET Framework) anlegt.

Neues Projekt erstellen

Mein Schiff habe ich hier Schulschiff genannt, weil es das Schiff zum Mitlernen ist. Gib deinem Schiff einen tolleren Namen!

Neues Projekt konfigurieren

Im weiteren Verwende ich .NET Core 3.1 – das ist je nach Installationsart noch ein weiterer Download und Installationsschritt. Damit hat sich die C#-Sprachversion auf 8.0 eingestellt. Da ich Konsolenausgabe sehen will, erstelle ich den Ausgabentyp Konsolenanwendung. Eine Forms-Oberfläche geht später dennoch. Die Eigenschaften unter Projekt → Eigenschaften sehen dann so aus:

Der Connector für die Verbindung zum Server ist im Moment über die Flattiverse Dokumentationsseite erreichbar – er kommt in das Connector-Verzeichnis im Projekt:

Connector im Verzeichnis

Ein Rechtsklick auf Abhängigkeiten öffnet den Verweis-Manager, in dem wir die Connector.dll im Verzeichnis suchen und einbinden.

Abhaengigkeiten
Verweis-Manager

Einmal auf RUN gedrückt zeigt uns, ob wir alles einigermaßen richtig zusammengestellt haben.