update
This commit is contained in:
@@ -1,15 +1,24 @@
|
||||
extends Node
|
||||
## Wechselt zwischen Menü und laufender Partie.
|
||||
## Wechselt zwischen Menü und laufenden Partien.
|
||||
##
|
||||
## Mit `-- server <port>` läuft dieselbe Datei als Raum auf dem Spielserver:
|
||||
## kein Menü, kein Bild, nur die Spiellogik.
|
||||
## Mit `-- server <port>` läuft dieselbe Datei als Spielserver: kein Menü, kein
|
||||
## Bild, dafür beliebig viele Räume nebeneinander.
|
||||
##
|
||||
## Ein Raum steckt in einem SubViewport mit eigener World3D. Nur so hat jede
|
||||
## Partie ihren eigenen Physikraum. Die Knotennamen sind auf Server und Client
|
||||
## gleich - daran hängt das RPC-Ziel.
|
||||
|
||||
var menu: CanvasLayer
|
||||
var game: Node3D
|
||||
var raeume: Node
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
Net.game_started.connect(_start_game)
|
||||
raeume = Node.new()
|
||||
raeume.name = "Raeume"
|
||||
add_child(raeume)
|
||||
|
||||
Net.game_started.connect(_raum_bauen)
|
||||
Net.room_closed.connect(_raum_abbauen)
|
||||
Net.server_closed.connect(_back_to_menu)
|
||||
|
||||
var args := OS.get_cmdline_user_args()
|
||||
@@ -31,19 +40,40 @@ func _unhandled_key_input(event: InputEvent) -> void:
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
|
||||
func _start_game(cfg: Dictionary) -> void:
|
||||
if game != null:
|
||||
func _raum_bauen(code: String, cfg: Dictionary) -> void:
|
||||
if raeume.has_node(code):
|
||||
return
|
||||
if menu != null:
|
||||
menu.queue_free()
|
||||
menu = null
|
||||
game = Node3D.new()
|
||||
|
||||
var raum := Node.new()
|
||||
raum.name = code
|
||||
raeume.add_child(raum)
|
||||
|
||||
var behaelter := SubViewportContainer.new()
|
||||
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.own_world_3d = true
|
||||
behaelter.add_child(sicht)
|
||||
|
||||
var game := Node3D.new()
|
||||
game.set_script(preload("res://scripts/game.gd"))
|
||||
game.name = "Game" # fester Pfad, sonst findet das RPC-Routing den Knoten nicht
|
||||
add_child(game)
|
||||
game.name = "Game"
|
||||
sicht.add_child(game)
|
||||
game.setup(cfg)
|
||||
|
||||
|
||||
func _raum_abbauen(code: String) -> void:
|
||||
var raum := raeume.get_node_or_null(NodePath(code))
|
||||
if raum != null:
|
||||
raum.queue_free()
|
||||
|
||||
|
||||
func _back_to_menu() -> void:
|
||||
if game != null:
|
||||
get_tree().change_scene_to_file("res://scenes/main.tscn")
|
||||
get_tree().change_scene_to_file("res://scenes/main.tscn")
|
||||
|
||||
273
scripts/net.gd
273
scripts/net.gd
@@ -1,61 +1,66 @@
|
||||
extends Node
|
||||
## Netzwerk: Räume liegen auf dem Spielserver, Beitritt läuft über einen Code.
|
||||
## Netzwerk: ein Server, viele Räume, Beitritt per Code.
|
||||
##
|
||||
## Der Server ist eine eigene headless Instanz und sitzt nicht mit am Tisch.
|
||||
## Er ist Peer 1 und damit die Autorität. Ein Spieler im Raum ist Eigentümer
|
||||
## und darf die Partie starten.
|
||||
## Der Server hält alle Räume in einem einzigen Prozess und lauscht auf einem
|
||||
## einzigen WebSocket-Port. Damit braucht es weder einen Portbereich noch einen
|
||||
## Prozess je Partie. Ein Raum ist ein Knoten unter /root/Main/Raeume; sein
|
||||
## SubViewport bringt einen eigenen Physikraum mit, sonst würden die Würfel
|
||||
## aller Partien im selben Raum liegen.
|
||||
##
|
||||
## Die Knotenpfade müssen auf Server und Client gleich heißen - daran hängt
|
||||
## das Ziel jedes RPC.
|
||||
|
||||
signal lobby_changed
|
||||
signal game_started(config: Dictionary)
|
||||
signal game_started(code: String, cfg: Dictionary)
|
||||
signal room_closed(code: String)
|
||||
signal connection_failed(reason: String)
|
||||
signal server_closed
|
||||
signal room_opened(code: String)
|
||||
|
||||
const PORT := 8910
|
||||
const MAX_HUMANS := 8
|
||||
const STANDARD_SERVER := "https://pirate-dice.marha.de"
|
||||
## Ohne Spieler schliesst sich ein Raum von selbst, sonst stehen Leichen herum
|
||||
const LEERLAUF := 120.0
|
||||
const STANDARD_SERVER := "wss://pirate-dice.marha.de"
|
||||
## Ohne I, O, 0 und 1: die verwechselt man beim Vorlesen
|
||||
const ALPHABET := "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
|
||||
const CODE_LAENGE := 4
|
||||
const SOLO := "SOLO"
|
||||
|
||||
# ── Client ───────────────────────────────────────────────────────────────────
|
||||
var player_name := "Spieler"
|
||||
## peer_id -> Name. Nur Menschen; der Server steht nicht drin.
|
||||
var lobby: Dictionary = {}
|
||||
var owner_id := 0
|
||||
var is_owner := false
|
||||
var room_code := ""
|
||||
var in_game := false
|
||||
## true in der headless Instanz auf dem Server
|
||||
var dedicated := false
|
||||
|
||||
var _leer_seit := 0.0
|
||||
# ── Server ───────────────────────────────────────────────────────────────────
|
||||
var dedicated := false
|
||||
## code -> {lobby: {peer: name}, owner: int, in_game: bool}
|
||||
var raeume: Dictionary = {}
|
||||
## peer -> code
|
||||
var peer_raum: Dictionary = {}
|
||||
|
||||
var _wunsch: Dictionary = {}
|
||||
var _rng := RandomNumberGenerator.new()
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
multiplayer.peer_connected.connect(_on_peer_connected)
|
||||
_rng.randomize()
|
||||
multiplayer.peer_disconnected.connect(_on_peer_disconnected)
|
||||
multiplayer.connected_to_server.connect(_on_connected)
|
||||
multiplayer.connection_failed.connect(_on_connect_fail)
|
||||
multiplayer.server_disconnected.connect(_on_server_disconnected)
|
||||
|
||||
|
||||
## Zum Entwickeln auf einen lokalen Matchmaker zeigen lassen:
|
||||
## PIRATE_DICE_SERVER=http://127.0.0.1:8020 godot
|
||||
## Zum Entwickeln auf den lokalen Server zeigen lassen:
|
||||
## PIRATE_DICE_SERVER=ws://127.0.0.1:8000 godot
|
||||
func server_url() -> String:
|
||||
var eigen := OS.get_environment("PIRATE_DICE_SERVER")
|
||||
return eigen if eigen != "" else STANDARD_SERVER
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- Räume
|
||||
# ── Verbinden ────────────────────────────────────────────────────────────────
|
||||
|
||||
## Neuen Raum anlegen lassen und gleich betreten.
|
||||
func create_room(pname: String) -> void:
|
||||
var raum := await _api(HTTPClient.METHOD_POST, "/api/raum")
|
||||
if raum.is_empty():
|
||||
connection_failed.emit("Spielserver nicht erreichbar.")
|
||||
return
|
||||
room_code = String(raum.get("code", ""))
|
||||
room_opened.emit(room_code)
|
||||
join_game(String(raum.get("host", "")), pname, int(raum.get("port", PORT)))
|
||||
_verbinden({"art": "erstellen"}, pname)
|
||||
|
||||
|
||||
func join_room(code: String, pname: String) -> void:
|
||||
@@ -63,52 +68,29 @@ func join_room(code: String, pname: String) -> void:
|
||||
if code == "":
|
||||
connection_failed.emit("Kein Code eingegeben.")
|
||||
return
|
||||
var raum := await _api(HTTPClient.METHOD_GET, "/api/raum/" + code)
|
||||
if raum.is_empty():
|
||||
connection_failed.emit("Den Code %s gibt es nicht." % code)
|
||||
_verbinden({"art": "beitreten", "code": code}, pname)
|
||||
|
||||
|
||||
## Der Wunsch wartet, bis die Verbindung steht - vorher kann er nicht raus.
|
||||
func _verbinden(wunsch: Dictionary, pname: String) -> void:
|
||||
player_name = pname
|
||||
_wunsch = wunsch
|
||||
var peer := WebSocketMultiplayerPeer.new()
|
||||
if peer.create_client(server_url()) != OK:
|
||||
connection_failed.emit("Server nicht erreichbar.")
|
||||
return
|
||||
room_code = code
|
||||
join_game(String(raum.get("host", "")), pname, int(raum.get("port", PORT)))
|
||||
multiplayer.multiplayer_peer = peer
|
||||
|
||||
|
||||
func _api(method: int, pfad: String) -> Dictionary:
|
||||
var req := HTTPRequest.new()
|
||||
add_child(req)
|
||||
req.timeout = 8.0
|
||||
if req.request(server_url() + pfad, PackedStringArray(), method) != OK:
|
||||
req.queue_free()
|
||||
return {}
|
||||
var antwort: Array = await req.request_completed
|
||||
req.queue_free()
|
||||
if int(antwort[1]) != 200:
|
||||
return {}
|
||||
var daten: Variant = JSON.parse_string(
|
||||
(antwort[3] as PackedByteArray).get_string_from_utf8())
|
||||
return daten if daten is Dictionary else {}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- Verbindung
|
||||
|
||||
## Nur die headless Instanz auf dem Server ruft das auf.
|
||||
func serve(port: int) -> bool:
|
||||
var peer := ENetMultiplayerPeer.new()
|
||||
if peer.create_server(port, MAX_HUMANS) != OK:
|
||||
var peer := WebSocketMultiplayerPeer.new()
|
||||
if peer.create_server(port) != OK:
|
||||
push_error("Port %d ist belegt." % port)
|
||||
return false
|
||||
multiplayer.multiplayer_peer = peer
|
||||
dedicated = true
|
||||
_leer_seit = _jetzt()
|
||||
print("Raum offen auf Port %d." % port)
|
||||
return true
|
||||
|
||||
|
||||
func join_game(host: String, pname: String, port: int = PORT) -> bool:
|
||||
player_name = pname
|
||||
var peer := ENetMultiplayerPeer.new()
|
||||
if peer.create_client(host, port) != OK:
|
||||
connection_failed.emit("Verbindung zu %s:%d fehlgeschlagen." % [host, port])
|
||||
return false
|
||||
multiplayer.multiplayer_peer = peer
|
||||
print("Server lauscht auf Port %d." % port)
|
||||
return true
|
||||
|
||||
|
||||
@@ -126,49 +108,21 @@ func leave() -> void:
|
||||
|
||||
func is_online() -> bool:
|
||||
var peer := multiplayer.multiplayer_peer
|
||||
return peer is ENetMultiplayerPeer \
|
||||
return peer != null \
|
||||
and peer.get_connection_status() != MultiplayerPeer.CONNECTION_DISCONNECTED
|
||||
|
||||
|
||||
## Ein leerer Raum kostet sonst dauerhaft Speicher auf dem Server.
|
||||
func _process(_delta: float) -> void:
|
||||
if not dedicated:
|
||||
return
|
||||
if lobby.is_empty():
|
||||
if _jetzt() - _leer_seit > LEERLAUF:
|
||||
print("Niemand mehr im Raum, wird geschlossen.")
|
||||
get_tree().quit()
|
||||
else:
|
||||
_leer_seit = _jetzt()
|
||||
|
||||
|
||||
func _jetzt() -> float:
|
||||
return Time.get_ticks_msec() / 1000.0
|
||||
|
||||
|
||||
func _on_peer_connected(id: int) -> void:
|
||||
if multiplayer.is_server():
|
||||
_push_lobby.rpc_id(id, lobby, owner_id)
|
||||
|
||||
|
||||
func _on_peer_disconnected(id: int) -> void:
|
||||
if not multiplayer.is_server():
|
||||
return
|
||||
lobby.erase(id)
|
||||
# Geht der Eigentümer, rückt der nächste nach - sonst startet niemand mehr
|
||||
if id == owner_id:
|
||||
owner_id = 0 if lobby.is_empty() else int(lobby.keys()[0])
|
||||
_push_lobby.rpc(lobby, owner_id)
|
||||
lobby_changed.emit()
|
||||
|
||||
|
||||
func _on_connected() -> void:
|
||||
_register.rpc_id(1, player_name)
|
||||
if _wunsch.get("art", "") == "erstellen":
|
||||
_req_create.rpc_id(1, player_name)
|
||||
else:
|
||||
_req_join.rpc_id(1, String(_wunsch.get("code", "")), player_name)
|
||||
_wunsch = {}
|
||||
|
||||
|
||||
func _on_connect_fail() -> void:
|
||||
multiplayer.multiplayer_peer = null
|
||||
connection_failed.emit("Raum nicht erreichbar.")
|
||||
connection_failed.emit("Server nicht erreichbar.")
|
||||
|
||||
|
||||
func _on_server_disconnected() -> void:
|
||||
@@ -180,31 +134,79 @@ func _on_server_disconnected() -> void:
|
||||
server_closed.emit()
|
||||
|
||||
|
||||
# ── Räume (nur Server) ───────────────────────────────────────────────────────
|
||||
|
||||
func _neuer_code() -> String:
|
||||
while true:
|
||||
var code := ""
|
||||
for i in CODE_LAENGE:
|
||||
code += ALPHABET[_rng.randi_range(0, ALPHABET.length() - 1)]
|
||||
if not raeume.has(code):
|
||||
return code
|
||||
return ""
|
||||
|
||||
|
||||
@rpc("any_peer", "reliable")
|
||||
func _register(pname: String) -> void:
|
||||
func _req_create(pname: String) -> void:
|
||||
if not multiplayer.is_server():
|
||||
return
|
||||
var id := multiplayer.get_remote_sender_id()
|
||||
if in_game:
|
||||
_abweisen(id, "In dem Raum läuft schon eine Partie.")
|
||||
return
|
||||
if lobby.size() >= MAX_HUMANS:
|
||||
_abweisen(id, "Der Raum ist voll.")
|
||||
return
|
||||
lobby[id] = pname.substr(0, 16)
|
||||
if owner_id == 0:
|
||||
owner_id = id
|
||||
_push_lobby.rpc(lobby, owner_id)
|
||||
lobby_changed.emit()
|
||||
var code := _neuer_code()
|
||||
raeume[code] = {"lobby": {id: pname.substr(0, 16)}, "owner": id, "in_game": false}
|
||||
peer_raum[id] = code
|
||||
print("Raum %s aufgemacht (%d offen)." % [code, raeume.size()])
|
||||
_lobby_senden(code)
|
||||
|
||||
|
||||
## Erst die Begründung schicken, dann trennen - andersherum kommt sie nie an.
|
||||
func _abweisen(id: int, grund: String) -> void:
|
||||
_abgewiesen.rpc_id(id, grund)
|
||||
await get_tree().create_timer(0.5).timeout
|
||||
var peer := multiplayer.multiplayer_peer
|
||||
if peer is ENetMultiplayerPeer:
|
||||
peer.disconnect_peer(id)
|
||||
@rpc("any_peer", "reliable")
|
||||
func _req_join(code: String, pname: String) -> void:
|
||||
if not multiplayer.is_server():
|
||||
return
|
||||
var id := multiplayer.get_remote_sender_id()
|
||||
code = code.strip_edges().to_upper()
|
||||
var raum: Dictionary = raeume.get(code, {})
|
||||
if raum.is_empty():
|
||||
_abgewiesen.rpc_id(id, "Den Code %s gibt es nicht." % code)
|
||||
return
|
||||
if raum.in_game:
|
||||
_abgewiesen.rpc_id(id, "In dem Raum läuft schon eine Partie.")
|
||||
return
|
||||
if raum.lobby.size() >= MAX_HUMANS:
|
||||
_abgewiesen.rpc_id(id, "Der Raum ist voll.")
|
||||
return
|
||||
raum.lobby[id] = pname.substr(0, 16)
|
||||
peer_raum[id] = code
|
||||
_lobby_senden(code)
|
||||
|
||||
|
||||
## Ein Broadcast würde alle Räume treffen, also gezielt an die eigenen Peers.
|
||||
func _lobby_senden(code: String) -> void:
|
||||
var raum: Dictionary = raeume.get(code, {})
|
||||
if raum.is_empty():
|
||||
return
|
||||
for peer: int in raum.lobby:
|
||||
_push_lobby.rpc_id(peer, code, raum.lobby, raum.owner)
|
||||
|
||||
|
||||
func _on_peer_disconnected(id: int) -> void:
|
||||
if not multiplayer.is_server():
|
||||
return
|
||||
var code: String = peer_raum.get(id, "")
|
||||
peer_raum.erase(id)
|
||||
var raum: Dictionary = raeume.get(code, {})
|
||||
if raum.is_empty():
|
||||
return
|
||||
raum.lobby.erase(id)
|
||||
# Der letzte macht das Licht aus: leerer Raum, weg damit.
|
||||
if raum.lobby.is_empty():
|
||||
raeume.erase(code)
|
||||
room_closed.emit(code)
|
||||
print("Raum %s zu (%d offen)." % [code, raeume.size()])
|
||||
return
|
||||
# Geht der Host, rückt der nächste nach - sonst startet niemand mehr
|
||||
if id == raum.owner:
|
||||
raum.owner = int(raum.lobby.keys()[0])
|
||||
_lobby_senden(code)
|
||||
|
||||
|
||||
@rpc("authority", "reliable")
|
||||
@@ -214,27 +216,30 @@ func _abgewiesen(grund: String) -> void:
|
||||
|
||||
|
||||
@rpc("authority", "reliable")
|
||||
func _push_lobby(data: Dictionary, besitzer: int) -> void:
|
||||
func _push_lobby(code: String, data: Dictionary, besitzer: int) -> void:
|
||||
room_code = code
|
||||
lobby = data
|
||||
owner_id = besitzer
|
||||
is_owner = besitzer == multiplayer.get_unique_id()
|
||||
lobby_changed.emit()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- Start
|
||||
# ── Start ────────────────────────────────────────────────────────────────────
|
||||
|
||||
## Einzelspieler: kein Netzwerk, nur der lokale Spieler und Bots.
|
||||
func start_singleplayer(pname: String, ai_count: int) -> void:
|
||||
player_name = pname
|
||||
multiplayer.multiplayer_peer = OfflineMultiplayerPeer.new()
|
||||
room_code = SOLO
|
||||
lobby = {1: pname}
|
||||
owner_id = 1
|
||||
is_owner = true
|
||||
in_game = true
|
||||
lobby_changed.emit()
|
||||
_begin({"peers": [1], "names": lobby.duplicate(), "ai": ai_count})
|
||||
game_started.emit(SOLO, {"peers": [1], "names": lobby.duplicate(), "ai": ai_count})
|
||||
|
||||
|
||||
## Der Eigentümer bittet den Server um den Start.
|
||||
## Der Host bittet den Server um den Start.
|
||||
func request_start(ai_count: int) -> void:
|
||||
_req_start.rpc_id(1, ai_count)
|
||||
|
||||
@@ -243,22 +248,24 @@ func request_start(ai_count: int) -> void:
|
||||
func _req_start(ai_count: int) -> void:
|
||||
if not multiplayer.is_server():
|
||||
return
|
||||
if multiplayer.get_remote_sender_id() != owner_id:
|
||||
var id := multiplayer.get_remote_sender_id()
|
||||
var code: String = peer_raum.get(id, "")
|
||||
var raum: Dictionary = raeume.get(code, {})
|
||||
if raum.is_empty() or raum.in_game or id != raum.owner:
|
||||
return
|
||||
start_game(ai_count)
|
||||
|
||||
|
||||
func start_game(ai_count: int) -> void:
|
||||
if not multiplayer.is_server() or in_game:
|
||||
return
|
||||
var order: Array = lobby.keys()
|
||||
var order: Array = raum.lobby.keys()
|
||||
order.sort()
|
||||
if order.size() + ai_count < 2:
|
||||
return
|
||||
_begin.rpc({"peers": order, "names": lobby.duplicate(), "ai": ai_count})
|
||||
raum.in_game = true
|
||||
var cfg := {"peers": order, "names": raum.lobby.duplicate(), "ai": ai_count}
|
||||
for peer: int in order:
|
||||
_begin.rpc_id(peer, code, cfg)
|
||||
game_started.emit(code, cfg)
|
||||
|
||||
|
||||
@rpc("authority", "call_local", "reliable")
|
||||
func _begin(cfg: Dictionary) -> void:
|
||||
@rpc("authority", "reliable")
|
||||
func _begin(code: String, cfg: Dictionary) -> void:
|
||||
in_game = true
|
||||
game_started.emit(cfg)
|
||||
room_code = code
|
||||
game_started.emit(code, cfg)
|
||||
|
||||
Reference in New Issue
Block a user