Spielstart zuverlaessig machen, Client wieder scharf rendern

Der Client steckte die Partie in einen SubViewport. Der rendert mit der
Basisaufloesung 1280x720 und wurde danach hochskaliert - das Bild wurde
matschig und die Kantenglaettung fiel weg. Den eigenen Physikraum braucht
aber nur der Server, weil dort viele Partien nebeneinander laufen. Also
bekommt nur er SubViewports; der Client haengt die Partie direkt in die
Szene. Die Knotennamen bleiben gleich, daran haengt das RPC-Ziel.

Der Healthcheck oeffnete alle 30 Sekunden eine nackte TCP-Verbindung auf
den WebSocket-Port. Der Server lehnte sie ab ("Missing or invalid header
'upgrade'"), und dabei ging reproduzierbar ein Paket an einen echten
Spieler verloren. Traefik braucht den Check nicht, also raus damit.

Blieb ein seltener Verlust des Startpakets: der Spieler sass in der Lobby,
waehrend die anderen spielten. Der Server legt jetzt nach, bis jeder den
Start quittiert hat.

Ein Verbindungsabbruch im Menue lud ausserdem die Szene neu und warf die
Meldung dazu gleich wieder weg.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
team3
2026-09-07 22:43:02 +02:00
parent 02247a2cbe
commit f97d741c91
3 changed files with 64 additions and 19 deletions

View File

@@ -4,15 +4,10 @@ services:
context: . context: .
container_name: pirate-dice container_name: pirate-dice
restart: unless-stopped restart: unless-stopped
healthcheck: # Kein Healthcheck: der Server spricht ausschliesslich WebSocket. Eine
# Der Server spricht WebSocket, kein HTTP - also nur schauen, ob der # nackte TCP-Verbindung darauf lehnt er ab ("Missing or invalid header
# Port jemanden zu Hause hat. # 'upgrade'") - und dabei ging reproduzierbar ein Paket an einen echten
test: ["CMD", "bash", "-c", "exec 3<>/dev/tcp/127.0.0.1/8000"] # Spieler verloren. Traefik braucht den Check nicht.
interval: 30s
timeout: 10s
retries: 3
start_period: 20s
start_interval: 2s
networks: networks:
- web - web
# Keine Portfreigabe nötig: alles läuft als WebSocket über Traefik auf 443. # Keine Portfreigabe nötig: alles läuft als WebSocket über Traefik auf 443.

View File

@@ -51,15 +51,28 @@ func _raum_bauen(code: String, cfg: Dictionary) -> void:
raum.name = code raum.name = code
raeume.add_child(raum) raeume.add_child(raum)
var behaelter := SubViewportContainer.new() var behaelter: Node
var sicht: Node
if Net.dedicated:
# Auf dem Server laufen viele Partien nebeneinander. Jede braucht ihren
# eigenen Physikraum, sonst lägen alle Würfel im selben.
var c := SubViewportContainer.new()
c.stretch = true
c.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
var v := SubViewport.new()
v.own_world_3d = true
behaelter = c
sicht = v
else:
# Der Client hat nur eine Partie und rendert direkt ins Fenster. Ein
# SubViewport würde hier nur Auflösung und Kantenglättung kosten.
# Die Knotennamen müssen trotzdem gleich heißen - daran hängt das
# Ziel jedes RPC.
behaelter = Node.new()
sicht = Node.new()
behaelter.name = "Container" behaelter.name = "Container"
behaelter.stretch = true
behaelter.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
raum.add_child(behaelter)
var sicht := SubViewport.new()
sicht.name = "Viewport" sicht.name = "Viewport"
sicht.own_world_3d = true raum.add_child(behaelter)
behaelter.add_child(sicht) behaelter.add_child(sicht)
var game := Node3D.new() var game := Node3D.new()
@@ -75,5 +88,9 @@ func _raum_abbauen(code: String) -> void:
raum.queue_free() raum.queue_free()
## Nur wenn wirklich eine Partie lief. Sonst würde ein Verbindungsabbruch im
## Menü die Szene neu laden und die Meldung dazu gleich wieder wegwerfen.
func _back_to_menu() -> void: func _back_to_menu() -> void:
if raeume.get_child_count() == 0:
return
get_tree().change_scene_to_file("res://scenes/main.tscn") get_tree().change_scene_to_file("res://scenes/main.tscn")

View File

@@ -152,7 +152,8 @@ func _req_create(pname: String) -> void:
return return
var id := multiplayer.get_remote_sender_id() var id := multiplayer.get_remote_sender_id()
var code := _neuer_code() var code := _neuer_code()
raeume[code] = {"lobby": {id: pname.substr(0, 16)}, "owner": id, "in_game": false} raeume[code] = {"lobby": {id: pname.substr(0, 16)}, "owner": id, "in_game": false,
"bestaetigt": {}}
peer_raum[id] = code peer_raum[id] = code
print("Raum %s aufgemacht (%d offen)." % [code, raeume.size()]) print("Raum %s aufgemacht (%d offen)." % [code, raeume.size()])
_lobby_senden(code) _lobby_senden(code)
@@ -259,13 +260,45 @@ func _req_start(ai_count: int) -> void:
return return
raum.in_game = true raum.in_game = true
var cfg := {"peers": order, "names": raum.lobby.duplicate(), "ai": ai_count} var cfg := {"peers": order, "names": raum.lobby.duplicate(), "ai": ai_count}
for peer: int in order: _start_zustellen(code, cfg)
_begin.rpc_id(peer, code, cfg)
game_started.emit(code, cfg) game_started.emit(code, cfg)
## Ein verlorenes Startpaket lässt den Spieler in der Lobby sitzen, während
## die anderen spielen. Deshalb wird nachgelegt, bis jeder quittiert hat.
func _start_zustellen(code: String, cfg: Dictionary) -> void:
for versuch in 4:
var raum: Dictionary = raeume.get(code, {})
if raum.is_empty():
return
var offen: Array = []
for peer: int in raum.lobby:
if not raum.bestaetigt.has(peer):
offen.append(peer)
if offen.is_empty():
return
if versuch > 0:
print("Start an %s noch einmal (Versuch %d)." % [str(offen), versuch + 1])
for peer: int in offen:
_begin.rpc_id(peer, code, cfg)
await get_tree().create_timer(1.5).timeout
@rpc("any_peer", "reliable")
func _start_erhalten() -> void:
if not multiplayer.is_server():
return
var id := multiplayer.get_remote_sender_id()
var raum: Dictionary = raeume.get(peer_raum.get(id, ""), {})
if not raum.is_empty():
raum.bestaetigt[id] = true
@rpc("authority", "reliable") @rpc("authority", "reliable")
func _begin(code: String, cfg: Dictionary) -> void: func _begin(code: String, cfg: Dictionary) -> void:
_start_erhalten.rpc_id(1)
if in_game and room_code == code:
return # Nachzügler-Paket, die Partie läuft hier schon
in_game = true in_game = true
room_code = code room_code = code
game_started.emit(code, cfg) game_started.emit(code, cfg)