[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).
This commit is contained in:
Fabio Alessandrelli
2022-09-29 21:00:44 +02:00
parent dd2ba9a5ba
commit da12c09942
6 changed files with 209 additions and 107 deletions

View File

@@ -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]

View File

@@ -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

View File

@@ -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"]

View File

@@ -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)

View File

@@ -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")

View File

@@ -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()