somethinglikegames.de

Mein Blog über Spieleentwicklung und das Drumherum


Kategorien


Tags

Der Quellcode zu den Artikeln ist in diesem GitHub Repository zu finden. Die folgende Liste zeigt alle bislang erschienenen Artikel dieser Serie:

  1. Netzwerktutorial 1: Allgemeines und Übersicht
  2. Netzwerktutorial 2: Das "gehende Skelett"
  3. Netzwerktutorial 3: Login 1 - Der Game-Client
  4. Netzwerktutorial 4: Login 2 - Gateway- und Authentication-Server
  5. Netzwerktutorial 5: Login 3 - World-Server
  6. Netzwerktutorial 6: Verschlüsselte Verbindungen mit Hilfe von DTLS

Ich habe wirklich lange über die richtige Vorgehensweise für dieses Tutorial nachgedacht. Schließlich gibt es verschiedene Möglichkeiten, wenn man, wie hier, die aktuell geplanten vier Komponenten entwickeln möchte. Ein Extremfall wäre es, pro Tutorial jeweils eine Komponente vollständig zu entwickeln und dadurch erst zum Schluss die Kommunikation zwischen den Komponenten zu ermöglichen. Ich habe mich für eine andere Variante entschieden und setze alle Komponenten direkt ohne wirkliche Funktionalität auf, um sie anschließend in jedem Tutorial, passend zum Thema, mit weiteren Inhalten zu füllen.

Diese Vorgehensweise nennt sich auch Walking Skeleton. Geprägt wurde der Begriff wohl Ende der 1990er Jahre durch Alistair Cockburn, einen der Pioniere der agilen Softwareentwicklung. Leider befindet sich seine Website zum Zeitpunkt der Erstellung dieses Artikels im Umbau, aber dank der Wayback Machine, habe ich dann doch noch das von mir gesuchte Zitat zum Walking Skeleton von ihm gefunden:

A Walking Skeleton is a tiny implementation of the system that performs a small end-to-end function. It need not use the final architecture, but it should link together the main architectural components. The architecture and the functionality can then evolve in parallel.

Der erste Schritt zum gehenden Skelett ist, alle notwendigen Projekte anzulegen. In unserem Fall sind es also die vier Godot-Projekte “Authentication Server”, “Gateway Server”, “World Server” und “Game Client”. Da die Server-Projekte alle als Headless-Projekte geplant sind, also ohne graphische Komponente, nutzen diese als Renderer Compatibility, während der Game Client Forward+ verwendet, aber der Renderer spielt für diese Tutorialserie eigentlich keine wirkliche Rolle. Theoretisch ist es auch möglich, alle Teilkomponenten innerhalb eines Godot-Projekts zu realisieren, aber durch die Modularisierung wird es nicht nur übersichtlicher, sondern auch sicherer, da der Game Client nur die für ihn notwendigen Informationen enthält.

Um die Server auch im Editor headless zu starten, muss man jeweils in Projekteinstellungen -> Editor -> Main Run Args den Wert --display-driver headless eintragen.

Genereller Projektaufbau

Damit die Projekte fehlerfrei per Netzwerk kommunizieren können, müssen die SceneTrees der beteiligten Parteien identisch aufgebaut sein. Das lässt sich am einfachsten erreichen, indem man die Projekte möglichst identisch aufbaut, daher nutze ich hier folgende Projektstruktur:

.
├── scenes
│   ├── network
│   │   ├── authentication_server.gd
│   │   ├── gateway_server.gd
│   │   └── world_server.gd
│   ├── main.gd
│   └── main.tscn
└── icon.svg

Welche Skripte innerhalb des “network”-Ordners vorhanden sind, hängt natürlich davon ab, welche Serverfunktionalität bereitgestellt wird bzw. mit welchem Server das jeweilige Projekt kommunizieren soll. Wenn man sich die geplante Architektur aus dem letzten Artikel nochmals in Erinnerung ruft und die Kommunikationswege betrachtet, kommt man auf die folgenden Skripte:

Authentication-Server
authentication_server.gd
Gateway-Server
authentication_server.gd, gateway_server.gd
World-Server
world_server.gd
Game Client
authentication_server.gd, world_server.gd

Das Server-Grundgerüst

Wir fangen mit folgendem Grundgerüst für die Server an:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
extends Node

@export_range(1025, 65536) var network_port := 1911
@export_range(2, 4095) var max_clients := 2


func startup() -> void:
	var network := ENetMultiplayerPeer.new()
	var ret := network.create_server(network_port, max_clients)
	if ret == OK:
		get_multiplayer().set_multiplayer_peer(network)
		print("Server started on port %d, allowing max %d connections"
				% [network_port, max_clients])

		network.peer_connected.connect(
			func(client_id: int) -> void:
				print("Client %d connected" % client_id)
				)
		network.peer_disconnected.connect(
			func(client_id: int) -> void:
				print("Client %d disconnected" % client_id)
				)
	else:
		print("Error while starting server: %d" % ret)
		get_tree().quit(ret)

In den Zeilen 3 und 4 werden die konfigurierbaren Parameter exportiert. So kann man sie auch direkt im Editor anpassen. Wichtig ist, dass jeder Server seinen eigenen Netzwerkport bekommt. In der Funktion startup() wird der ENet-Server gestartet. Dafür erstellen wir in Zeile 8 zunächst ein neues Objekt vom Typ ENetMultiplayerPeer, um damit in Zeile 9 den ENet-Server zu starten. Falls dies erfolgreich war, hinterlegen wir unseren gestarteten ENet-Server in unserem aktuellen MultiplayerAPI-Objekt, geben eine kurze Erfolgsmeldung aus und verknüpfen anschließend die Signale, um mitzubekommen, wenn sich jemand verbunden bzw. die Verbindung getrennt hat (Zeilen 11-22). Zum Start nutzen wir dafür einfache Lambdas, die uns aktuell erst einmal nur Logmeldungen ausgeben. Sollte beim Start des Servers etwas schiefgelaufen sein, geben wir eine Fehlermeldung aus und beenden anschließend den Prozess (Zeilen 23-24).

Das Startszene der Server

SceneTree des Authentication-Servers

SceneTree des Authentication-Servers.

main.tscn wird bei allen Projekten als Startszene genutzt und startet entweder die jeweiligen Netzwerkkomponenten oder fungiert im Client als SceneHandler, welcher als zentrale Komponente für das Management von Szenen und (Netzwerk-)Ressourcen dient. Daher kann der Aufbau von main.tscn auf dem Server simpel gehalten werden. Wie im Bild gezeigt, ist es ein Node namens “Main” mit Nodes für jede Netzwerkkomponente, die das zugehörige Netzwerkskript verknüpft haben. Beim Authentication-Server sieht das dann so aus:

1
2
3
4
5
6
7
8
9
extends Node

@onready var authentication_server := $AuthenticationServer


func _ready() -> void:
	get_tree().set_multiplayer(SceneMultiplayer.new(),
		^"/root/Main/AuthenticationServer")
	authentication_server.startup()

Wenn man Erfahrungen mit Godot 3.x hat, werden die Zeilen 7-8 wahrscheinlich etwas ungewöhnlich aussehen. Mit Godot 4 wurde custom_multiplayer entfernt und stattdessen wurde die Funktion set_multiplayer im SceneTree so erweitert, dass man nun unterschiedliche MultiplayerAPI-Objekte abhängig vom NodePath hinterlegen kann, um mehr als eine Netzwerkkomponente betreiben/nutzen zu können. Das ist zwar eigentlich im Authentication-Server gar nicht notwendig, schließlich besitzt er nur eine Netzwerkkomponente, aber bei meinen Experimenten habe ich feststellen dürfen, dass es zu Kommunikationsproblemen zwischen Gateway- und Authentication-Server geführt hat, wenn der Authentication-Server nicht auf diese Weise konfiguriert war.

Auf eine ähnliche Weise sind auch die anderen zwei Server aufgebaut. Beim Gateway-Server verwenden wir den Netzwerkport 1910 und beim World-Server 1909. Der bisherige Quellcode wird natürlich auch im GitHub Repository abgebildet. Im nächsten Artikel werden wir dann anfangen, den Login-Vorgang des Game Clients zu realisieren.