From da12c0994203644be9eb5ffb396122424c5de7a0 Mon Sep 17 00:00:00 2001 From: Fabio Alessandrelli Date: Thu, 29 Sep 2022 21:00:44 +0200 Subject: [PATCH] [Net] Update WebSocket multiplayer demo. Updated to Godot 4. Added combo (default) scene showing dedicated multiplayer branches (i.e. running both server and clients in the same SceneTree). --- .../websocket_multiplayer/project.godot | 5 +- .../websocket_multiplayer/scene/combo.tscn | 66 ++++++++++++ .../websocket_multiplayer/scene/main.tscn | 100 ++++++++++-------- .../websocket_multiplayer/script/combo.gd | 16 +++ .../websocket_multiplayer/script/game.gd | 78 ++++++++------ .../websocket_multiplayer/script/main.gd | 51 ++++----- 6 files changed, 209 insertions(+), 107 deletions(-) create mode 100644 networking/websocket_multiplayer/scene/combo.tscn create mode 100644 networking/websocket_multiplayer/script/combo.gd diff --git a/networking/websocket_multiplayer/project.godot b/networking/websocket_multiplayer/project.godot index 4612ad05..0dfcdede 100644 --- a/networking/websocket_multiplayer/project.godot +++ b/networking/websocket_multiplayer/project.godot @@ -6,13 +6,14 @@ ; [section] ; section goes between [] ; param=value ; assign values to parameters -config_version=4 +config_version=5 [application] config/name="WebSocket Multiplayer Demo" config/description="This is a sample showing how the use WebSockets along with the Multiplayer API in Godot." -run/main_scene="res://scene/main.tscn" +run/main_scene="res://scene/combo.tscn" +config/features=PackedStringArray("4.0") config/icon="res://icon.png" [rendering] diff --git a/networking/websocket_multiplayer/scene/combo.tscn b/networking/websocket_multiplayer/scene/combo.tscn new file mode 100644 index 00000000..ebe73ba6 --- /dev/null +++ b/networking/websocket_multiplayer/scene/combo.tscn @@ -0,0 +1,66 @@ +[gd_scene load_steps=3 format=3 uid="uid://cwmhra3pt1h83"] + +[ext_resource type="Script" path="res://script/combo.gd" id="1_8i0ov"] +[ext_resource type="PackedScene" uid="uid://c240icwf4uov8" path="res://scene/main.tscn" id="2_reiiv"] + +[node name="Combo" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_8i0ov") + +[node name="GridContainer" type="GridContainer" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +columns = 2 + +[node name="Main" parent="GridContainer" instance=ExtResource("2_reiiv")] +layout_mode = 2 +anchors_preset = 0 +anchor_right = 0.0 +anchor_bottom = 0.0 +offset_right = 574.0 +offset_bottom = 322.0 +grow_horizontal = 1 +grow_vertical = 1 + +[node name="Main2" parent="GridContainer" instance=ExtResource("2_reiiv")] +layout_mode = 2 +anchors_preset = 0 +anchor_right = 0.0 +anchor_bottom = 0.0 +offset_left = 578.0 +offset_right = 1152.0 +offset_bottom = 322.0 +grow_horizontal = 1 +grow_vertical = 1 + +[node name="Main3" parent="GridContainer" instance=ExtResource("2_reiiv")] +layout_mode = 2 +anchors_preset = 0 +anchor_right = 0.0 +anchor_bottom = 0.0 +offset_top = 326.0 +offset_right = 574.0 +offset_bottom = 648.0 +grow_horizontal = 1 +grow_vertical = 1 + +[node name="Main4" parent="GridContainer" instance=ExtResource("2_reiiv")] +layout_mode = 2 +anchors_preset = 0 +anchor_right = 0.0 +anchor_bottom = 0.0 +offset_left = 578.0 +offset_top = 326.0 +offset_right = 1152.0 +offset_bottom = 648.0 +grow_horizontal = 1 +grow_vertical = 1 diff --git a/networking/websocket_multiplayer/scene/main.tscn b/networking/websocket_multiplayer/scene/main.tscn index 8c36cce8..3871f234 100644 --- a/networking/websocket_multiplayer/scene/main.tscn +++ b/networking/websocket_multiplayer/scene/main.tscn @@ -1,72 +1,83 @@ -[gd_scene load_steps=3 format=2] +[gd_scene load_steps=3 format=3 uid="uid://c240icwf4uov8"] -[ext_resource path="res://script/main.gd" type="Script" id=1] -[ext_resource path="res://scene/game.tscn" type="PackedScene" id=2] +[ext_resource type="Script" path="res://script/main.gd" id="1"] +[ext_resource type="PackedScene" path="res://scene/game.tscn" id="2"] [node name="Main" type="Control"] +layout_mode = 3 +anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 -script = ExtResource( 1 ) -__meta__ = { -"_edit_use_anchors_": false -} +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +script = ExtResource("1") [node name="Panel" type="Panel" parent="."] +anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 [node name="VBoxContainer" type="VBoxContainer" parent="Panel"] +anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 offset_left = 20.0 offset_top = 20.0 offset_right = -20.0 offset_bottom = -20.0 +grow_horizontal = 2 +grow_vertical = 2 [node name="HBoxContainer" type="HBoxContainer" parent="Panel/VBoxContainer"] -offset_right = 984.0 -offset_bottom = 24.0 +offset_right = 1112.0 +offset_bottom = 31.0 [node name="Label" type="Label" parent="Panel/VBoxContainer/HBoxContainer"] -offset_top = 5.0 -offset_right = 326.0 -offset_bottom = 19.0 +offset_top = 2.0 +offset_right = 369.0 +offset_bottom = 28.0 size_flags_horizontal = 3 text = "Name" [node name="NameEdit" type="LineEdit" parent="Panel/VBoxContainer/HBoxContainer"] -offset_left = 330.0 -offset_right = 984.0 -offset_bottom = 24.0 +offset_left = 373.0 +offset_right = 1112.0 +offset_bottom = 31.0 size_flags_horizontal = 3 size_flags_stretch_ratio = 2.0 text = "A Godot User" [node name="HBoxContainer2" type="HBoxContainer" parent="Panel/VBoxContainer"] -offset_top = 28.0 -offset_right = 984.0 -offset_bottom = 52.0 +offset_top = 35.0 +offset_right = 1112.0 +offset_bottom = 66.0 [node name="HBoxContainer" type="HBoxContainer" parent="Panel/VBoxContainer/HBoxContainer2"] -offset_right = 326.0 -offset_bottom = 24.0 +offset_right = 369.0 +offset_bottom = 31.0 size_flags_horizontal = 3 [node name="Host" type="Button" parent="Panel/VBoxContainer/HBoxContainer2/HBoxContainer"] -offset_right = 42.0 -offset_bottom = 24.0 +offset_right = 44.0 +offset_bottom = 31.0 text = "Host" [node name="Control" type="Control" parent="Panel/VBoxContainer/HBoxContainer2/HBoxContainer"] -offset_left = 46.0 -offset_right = 241.0 -offset_bottom = 24.0 +layout_mode = 3 +anchors_preset = 0 +offset_left = 48.0 +offset_right = 273.0 +offset_bottom = 31.0 size_flags_horizontal = 3 [node name="Connect" type="Button" parent="Panel/VBoxContainer/HBoxContainer2/HBoxContainer"] -offset_left = 245.0 -offset_right = 326.0 -offset_bottom = 24.0 +offset_left = 277.0 +offset_right = 369.0 +offset_bottom = 31.0 text = "Connect to" [node name="Disconnect" type="Button" parent="Panel/VBoxContainer/HBoxContainer2/HBoxContainer"] @@ -77,36 +88,31 @@ offset_bottom = 24.0 text = "Disconnect" [node name="Hostname" type="LineEdit" parent="Panel/VBoxContainer/HBoxContainer2"] -offset_left = 330.0 -offset_right = 984.0 -offset_bottom = 24.0 +offset_left = 373.0 +offset_right = 1112.0 +offset_bottom = 31.0 size_flags_horizontal = 3 size_flags_stretch_ratio = 2.0 text = "localhost" placeholder_text = "localhost" [node name="Control" type="Control" parent="Panel/VBoxContainer"] -offset_top = 56.0 -offset_right = 984.0 -offset_bottom = 76.0 -rect_min_size = Vector2(0, 20) +layout_mode = 3 +anchors_preset = 0 +offset_top = 70.0 +offset_right = 1112.0 +offset_bottom = 70.0 -[node name="Game" parent="Panel/VBoxContainer" instance=ExtResource( 2 )] +[node name="Game" parent="Panel/VBoxContainer" instance=ExtResource("2")] +layout_mode = 3 +anchors_preset = 0 anchor_right = 0.0 anchor_bottom = 0.0 -offset_top = 80.0 -offset_right = 984.0 -offset_bottom = 560.0 +offset_top = 74.0 +offset_right = 1112.0 +offset_bottom = 608.0 [node name="AcceptDialog" type="AcceptDialog" parent="."] -anchor_left = 0.5 -anchor_top = 0.5 -anchor_right = 0.5 -anchor_bottom = 0.5 -offset_left = -200.0 -offset_top = -100.0 -offset_right = 200.0 -offset_bottom = 100.0 dialog_text = "Connection closed" [connection signal="pressed" from="Panel/VBoxContainer/HBoxContainer2/HBoxContainer/Host" to="." method="_on_Host_pressed"] diff --git a/networking/websocket_multiplayer/script/combo.gd b/networking/websocket_multiplayer/script/combo.gd new file mode 100644 index 00000000..04f46034 --- /dev/null +++ b/networking/websocket_multiplayer/script/combo.gd @@ -0,0 +1,16 @@ +extends Control + +var paths := [] + +func _enter_tree(): + for ch in $GridContainer.get_children(): + paths.append(NodePath(str(get_path()) + "/GridContainer/" + str(ch.name))) + # Sets a dedicated Multiplayer API for each branch. + for path in paths: + get_tree().set_multiplayer(MultiplayerAPI.create_default_interface(), path) + + +func _exit_tree(): + # Clear the branch-specific Multiplayer API. + for path in paths: + get_tree().set_multiplayer(null, path) diff --git a/networking/websocket_multiplayer/script/game.gd b/networking/websocket_multiplayer/script/game.gd index 53a1fee4..9bd45b6c 100644 --- a/networking/websocket_multiplayer/script/game.gd +++ b/networking/websocket_multiplayer/script/game.gd @@ -5,36 +5,50 @@ const _crown = preload("res://img/crown.png") @onready var _list = $HBoxContainer/VBoxContainer/ItemList @onready var _action = $HBoxContainer/VBoxContainer/Action +const ACTIONS = ["roll", "pass"] + var _players = [] var _turn = -1 -master func set_player_name(name): - var sender = get_tree().get_rpc_sender_id() - rpc("update_player_name", sender, name) +@rpc func _log(what): + $HBoxContainer/RichTextLabel.add_text(what + "\n") -remotesync func update_player_name(player, name): +@rpc(any_peer) func set_player_name(name): + if not is_multiplayer_authority(): + return + var sender = multiplayer.get_remote_sender_id() + update_player_name.rpc(sender, name) + + +@rpc(call_local) func update_player_name(player, name): var pos = _players.find(player) if pos != -1: _list.set_item_text(pos, name) -master func request_action(action): - var sender = get_tree().get_rpc_sender_id() - if _players[_turn] != get_tree().get_rpc_sender_id(): - rpc("_log", "Someone is trying to cheat! %s" % str(sender)) +@rpc(any_peer) func request_action(action): + if not is_multiplayer_authority(): return + var sender = multiplayer.get_remote_sender_id() + if _players[_turn] != sender: + _log.rpc("Someone is trying to cheat! %s" % str(sender)) + return + if action not in ACTIONS: + _log.rpc("Invalid action: %s" % action) + return + do_action(action) next_turn() -remotesync func do_action(action): - var name = _list.get_item_text(_turn) +func do_action(action): + var player_name = _list.get_item_text(_turn) var val = randi() % 100 - rpc("_log", "%s: %ss %d" % [name, action, val]) + _log.rpc("%s: %ss %d" % [player_name, action, val]) -remotesync func set_turn(turn): +@rpc(call_local) func set_turn(turn): _turn = turn if turn >= _players.size(): return @@ -43,27 +57,27 @@ remotesync func set_turn(turn): _list.set_item_icon(i, _crown) else: _list.set_item_icon(i, null) - _action.disabled = _players[turn] != get_tree().get_network_unique_id() + _action.disabled = _players[turn] != multiplayer.get_unique_id() -remotesync func del_player(id): +@rpc(call_local) func del_player(id): var pos = _players.find(id) if pos == -1: return - _players.remove(pos) + _players.erase(pos) _list.remove_item(pos) if _turn > pos: _turn -= 1 - if get_tree().is_network_server(): - rpc("set_turn", _turn) + if multiplayer.is_server(): + set_turn.rpc(_turn) -remotesync func add_player(id, name=""): +@rpc(call_local) func add_player(id, pname=""): _players.append(id) - if name == "": + if pname == "": _list.add_item("... connecting ...", null, false) else: - _list.add_item(name, null, false) + _list.add_item(pname, null, false) func get_player_name(pos): @@ -77,7 +91,7 @@ func next_turn(): _turn += 1 if _turn >= _players.size(): _turn = 0 - rpc("set_turn", _turn) + set_turn.rpc(_turn) func start(): @@ -92,27 +106,25 @@ func stop(): func on_peer_add(id): - if not get_tree().is_network_server(): + if not multiplayer.is_server(): return for i in range(0, _players.size()): - rpc_id(id, "add_player", _players[i], get_player_name(i)) - rpc("add_player", id) - rpc_id(id, "set_turn", _turn) + add_player.rpc_id(id, _players[i], get_player_name(i)) + add_player.rpc(id) + set_turn.rpc_id(id, _turn) func on_peer_del(id): - if not get_tree().is_network_server(): + if not multiplayer.is_server(): return - rpc("del_player", id) - - -remotesync func _log(what): - $HBoxContainer/RichTextLabel.add_text(what + "\n") + del_player.rpc(id) func _on_Action_pressed(): - if get_tree().is_network_server(): + if multiplayer.is_server(): + if _turn != 0: + return do_action("roll") next_turn() else: - rpc_id(1, "request_action", "roll") + request_action.rpc_id(1, "roll") diff --git a/networking/websocket_multiplayer/script/main.gd b/networking/websocket_multiplayer/script/main.gd index 69eaa645..001808d4 100644 --- a/networking/websocket_multiplayer/script/main.gd +++ b/networking/websocket_multiplayer/script/main.gd @@ -10,14 +10,22 @@ const PROTO_NAME = "ludus" @onready var _host_edit = $Panel/VBoxContainer/HBoxContainer2/Hostname @onready var _game = $Panel/VBoxContainer/Game -var peer = null +var peer = WebSocketMultiplayerPeer.new() + + +func _init(): + peer.supported_protocols = ["ludus"] + func _ready(): - #warning-ignore-all:return_value_discarded - get_tree().connect(&"network_peer_disconnected", self._peer_disconnected) - get_tree().connect(&"network_peer_connected", self._peer_connected) - $AcceptDialog.get_label().align = Label.ALIGN_CENTER - $AcceptDialog.get_label().valign = Label.VALIGN_CENTER + multiplayer.peer_connected.connect(_peer_connected) + multiplayer.peer_disconnected.connect(_peer_disconnected) + multiplayer.server_disconnected.connect(_close_network) + multiplayer.connection_failed.connect(_close_network) + multiplayer.connected_to_server.connect(_connected) + + $AcceptDialog.get_label().horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + $AcceptDialog.get_label().vertical_alignment = VERTICAL_ALIGNMENT_CENTER # Set the player name according to the system username. Fallback to the path. if OS.has_environment("USERNAME"): _name_edit.text = OS.get_environment("USERNAME") @@ -45,20 +53,15 @@ func stop_game(): func _close_network(): - if get_tree().is_connected("server_disconnected", self, "_close_network"): - get_tree().disconnect(&"server_disconnected", self._close_network) - if get_tree().is_connected("connection_failed", self, "_close_network"): - get_tree().disconnect(&"connection_failed", self._close_network) - if get_tree().is_connected("connected_to_server", self, "_connected"): - get_tree().disconnect(&"connected_to_server", self._connected) stop_game() - $AcceptDialog.show_modal() - $AcceptDialog.get_close_button().grab_focus() - get_tree().set_network_peer(null) + $AcceptDialog.popup_centered() + $AcceptDialog.get_ok_button().grab_focus() + multiplayer.multiplayer_peer = null + peer.close() func _connected(): - _game.rpc("set_player_name", _name_edit.text) + _game.set_player_name.rpc(_name_edit.text) func _peer_connected(id): @@ -66,14 +69,14 @@ func _peer_connected(id): func _peer_disconnected(id): + print("Disconnected %d" % id) _game.on_peer_del(id) func _on_Host_pressed(): - peer = WebSocketServer.new() - peer.listen(DEF_PORT, PackedStringArray(["ludus"]), true) - get_tree().connect(&"server_disconnected", self._close_network) - get_tree().set_network_peer(peer) + multiplayer.multiplayer_peer = null + peer.create_server(DEF_PORT) + multiplayer.multiplayer_peer = peer _game.add_player(1, _name_edit.text) start_game() @@ -83,9 +86,7 @@ func _on_Disconnect_pressed(): func _on_Connect_pressed(): - peer = WebSocketClient.new() - peer.connect_to_url("ws://" + _host_edit.text + ":" + str(DEF_PORT), PackedStringArray([PROTO_NAME]), true) - get_tree().connect(&"connection_failed", self._close_network) - get_tree().connect(&"connected_to_server", self._connected) - get_tree().set_network_peer(peer) + multiplayer.multiplayer_peer = null + peer.create_client("ws://" + _host_edit.text + ":" + str(DEF_PORT)) + multiplayer.multiplayer_peer = peer start_game()