Browse Source

Merge pull request #343 from Faless/websocket/initial

Add 3 WebSocket demos
Rémi Verschelde 6 years ago
parent
commit
9b2da1fa2f

+ 73 - 0
networking/websocket_chat/client/client.gd

@@ -0,0 +1,73 @@
+extends Node
+
+onready var _log_dest = get_parent().get_node("Panel/VBoxContainer/RichTextLabel")
+
+var _client = WebSocketClient.new()
+var _write_mode = WebSocketPeer.WRITE_MODE_BINARY
+var _use_multiplayer = true
+var last_connected_client = 0
+
+func _init():
+	_client.connect("connection_established", self, "_client_connected")
+	_client.connect("connection_error", self, "_client_disconnected")
+	_client.connect("connection_closed", self, "_client_disconnected")
+	_client.connect("server_close_request", self, "_client_close_request")
+	_client.connect("data_received", self, "_client_received")
+
+	_client.connect("peer_packet", self, "_client_received")
+	_client.connect("peer_connected", self, "_peer_connected")
+	_client.connect("connection_succeeded", self, "_client_connected", ["multiplayer_protocol"])
+	_client.connect("connection_failed", self, "_client_disconnected")
+
+func _client_close_request(code, reason):
+	Utils._log(_log_dest, "Close code: %d, reason: %s" % [code, reason])
+
+func _peer_connected(id):
+	Utils._log(_log_dest, "%s: Client just connected" % id)
+	last_connected_client = id
+
+func _exit_tree():
+	_client.disconnect_from_host(1001, "Bye bye!")
+
+func _process(delta):
+	if _client.get_connection_status() == WebSocketClient.CONNECTION_DISCONNECTED:
+		return
+
+	_client.poll()
+
+func _client_connected(protocol):
+	Utils._log(_log_dest, "Client just connected with protocol: %s" % protocol)
+	_client.get_peer(1).set_write_mode(_write_mode)
+
+func _client_disconnected(clean=true):
+	Utils._log(_log_dest, "Client just disconnected. Was clean: %s" % clean)
+
+func _client_received(p_id = 1):
+	if _use_multiplayer:
+		var peer_id = _client.get_packet_peer()
+		var packet = _client.get_packet()
+		Utils._log(_log_dest, "MPAPI: From %s Data: %s" % [str(peer_id), Utils.decode_data(packet, false)])
+	else:
+		var packet = _client.get_peer(1).get_packet()
+		var is_string = _client.get_peer(1).was_string_packet()
+		Utils._log(_log_dest, "Received data. BINARY: %s: %s" % [not is_string, Utils.decode_data(packet, is_string)])
+
+func connect_to_url(host, protocols, multiplayer):
+	_use_multiplayer = multiplayer
+	if _use_multiplayer:
+		_write_mode = WebSocketPeer.WRITE_MODE_BINARY
+	return _client.connect_to_url(host, protocols, multiplayer)
+
+func disconnect_from_host():
+	_client.disconnect_from_host(1000, "Bye bye!")
+
+func send_data(data, dest):
+	_client.get_peer(1).set_write_mode(_write_mode)
+	if _use_multiplayer:
+		_client.set_target_peer(dest)
+		_client.put_packet(Utils.encode_data(data, _write_mode))
+	else:
+		_client.get_peer(1).put_packet(Utils.encode_data(data, _write_mode))
+
+func set_write_mode(mode):
+	_write_mode = mode

+ 130 - 0
networking/websocket_chat/client/client.tscn

@@ -0,0 +1,130 @@
+[gd_scene load_steps=3 format=2]
+
+[ext_resource path="res://client/client_ui.gd" type="Script" id=1]
+[ext_resource path="res://client/client.gd" type="Script" id=2]
+
+[node name="Client" type="Control"]
+anchor_right = 1.0
+anchor_bottom = 1.0
+script = ExtResource( 1 )
+__meta__ = {
+"_edit_use_anchors_": false
+}
+
+[node name="Panel" type="Panel" parent="."]
+anchor_right = 1.0
+anchor_bottom = 1.0
+__meta__ = {
+
+}
+
+[node name="VBoxContainer" type="VBoxContainer" parent="Panel"]
+anchor_right = 1.0
+anchor_bottom = 1.0
+__meta__ = {
+
+}
+
+[node name="Connect" type="HBoxContainer" parent="Panel/VBoxContainer"]
+margin_right = 1024.0
+margin_bottom = 24.0
+__meta__ = {
+
+}
+
+[node name="Host" type="LineEdit" parent="Panel/VBoxContainer/Connect"]
+margin_right = 956.0
+margin_bottom = 24.0
+size_flags_horizontal = 3
+text = "ws://localhost:8000/test/"
+placeholder_text = "ws://my.server/path/"
+__meta__ = {
+
+}
+
+[node name="Connect" type="Button" parent="Panel/VBoxContainer/Connect"]
+margin_left = 960.0
+margin_right = 1024.0
+margin_bottom = 24.0
+toggle_mode = true
+text = "Connect"
+__meta__ = {
+
+}
+
+[node name="Settings" type="HBoxContainer" parent="Panel/VBoxContainer"]
+margin_top = 28.0
+margin_right = 1024.0
+margin_bottom = 52.0
+__meta__ = {
+
+}
+
+[node name="Mode" type="OptionButton" parent="Panel/VBoxContainer/Settings"]
+margin_right = 41.0
+margin_bottom = 24.0
+__meta__ = {
+
+}
+
+[node name="Multiplayer" type="CheckBox" parent="Panel/VBoxContainer/Settings"]
+margin_left = 45.0
+margin_right = 171.0
+margin_bottom = 24.0
+pressed = true
+text = "Multiplayer API"
+__meta__ = {
+
+}
+
+[node name="Destination" type="OptionButton" parent="Panel/VBoxContainer/Settings"]
+margin_left = 175.0
+margin_right = 216.0
+margin_bottom = 24.0
+__meta__ = {
+
+}
+
+[node name="Send" type="HBoxContainer" parent="Panel/VBoxContainer"]
+margin_top = 56.0
+margin_right = 1024.0
+margin_bottom = 80.0
+__meta__ = {
+
+}
+
+[node name="LineEdit" type="LineEdit" parent="Panel/VBoxContainer/Send"]
+margin_right = 977.0
+margin_bottom = 24.0
+size_flags_horizontal = 3
+placeholder_text = "Enter some text to send..."
+__meta__ = {
+
+}
+
+[node name="Send" type="Button" parent="Panel/VBoxContainer/Send"]
+margin_left = 981.0
+margin_right = 1024.0
+margin_bottom = 24.0
+text = "Send"
+__meta__ = {
+
+}
+
+[node name="RichTextLabel" type="RichTextLabel" parent="Panel/VBoxContainer"]
+margin_top = 84.0
+margin_right = 1024.0
+margin_bottom = 600.0
+size_flags_vertical = 3
+__meta__ = {
+
+}
+
+[node name="Client" type="Node" parent="."]
+script = ExtResource( 2 )
+__meta__ = {
+
+}
+[connection signal="toggled" from="Panel/VBoxContainer/Connect/Connect" to="." method="_on_Connect_toggled"]
+[connection signal="item_selected" from="Panel/VBoxContainer/Settings/Mode" to="." method="_on_Mode_item_selected"]
+[connection signal="pressed" from="Panel/VBoxContainer/Send/Send" to="." method="_on_Send_pressed"]

+ 59 - 0
networking/websocket_chat/client/client_ui.gd

@@ -0,0 +1,59 @@
+extends Control
+
+onready var _client = get_node("Client")
+onready var _log_dest = get_node("Panel/VBoxContainer/RichTextLabel")
+onready var _line_edit = get_node("Panel/VBoxContainer/Send/LineEdit")
+onready var _host = get_node("Panel/VBoxContainer/Connect/Host")
+onready var _multiplayer = get_node("Panel/VBoxContainer/Settings/Multiplayer")
+onready var _write_mode = get_node("Panel/VBoxContainer/Settings/Mode")
+onready var _destination = get_node("Panel/VBoxContainer/Settings/Destination")
+
+func _ready():
+	_write_mode.clear()
+	_write_mode.add_item("BINARY")
+	_write_mode.set_item_metadata(0, WebSocketPeer.WRITE_MODE_BINARY)
+	_write_mode.add_item("TEXT")
+	_write_mode.set_item_metadata(1, WebSocketPeer.WRITE_MODE_TEXT)
+
+	_destination.add_item("Broadcast")
+	_destination.set_item_metadata(0, 0)
+	_destination.add_item("Last connected")
+	_destination.set_item_metadata(1, 1)
+	_destination.add_item("All But last connected")
+	_destination.set_item_metadata(2, -1)
+	_destination.select(0)
+
+func _on_Mode_item_selected( ID ):
+	_client.set_write_mode(_write_mode.get_selected_metadata())
+
+func _on_Send_pressed():
+	if _line_edit.text == "":
+		return
+
+	var dest = _destination.get_selected_metadata()
+	if dest > 0:
+		dest = _client.last_connected_client
+	elif dest < 0:
+		dest = -_client.last_connected_client
+
+	Utils._log(_log_dest, "Sending data %s to %s" % [_line_edit.text, dest])
+	_client.send_data(_line_edit.text, dest)
+	_line_edit.text = ""
+
+func _on_Connect_toggled( pressed ):
+	if pressed:
+		var multiplayer = _multiplayer.pressed
+		if multiplayer:
+			_write_mode.disabled = true
+		else:
+			_destination.disabled = true
+		_multiplayer.disabled = true
+		if _host.text != "":
+			Utils._log(_log_dest, "Connecting to host: %s" % [_host.text])
+			var supported_protocols = PoolStringArray(["my-protocol2", "my-protocol", "binary"])
+			_client.connect_to_url(_host.text, supported_protocols, multiplayer)
+	else:
+		_destination.disabled = false
+		_write_mode.disabled = false
+		_multiplayer.disabled = false
+		_client.disconnect_from_host()

+ 62 - 0
networking/websocket_chat/combo/combo.tscn

@@ -0,0 +1,62 @@
+[gd_scene load_steps=3 format=2]
+
+[ext_resource path="res://server/server.tscn" type="PackedScene" id=1]
+[ext_resource path="res://client/client.tscn" type="PackedScene" id=2]
+
+[node name="Combo" type="Control"]
+anchor_right = 1.0
+anchor_bottom = 1.0
+mouse_filter = 1
+__meta__ = {
+
+}
+
+[node name="Box" type="HBoxContainer" parent="."]
+anchor_right = 1.0
+anchor_bottom = 1.0
+custom_constants/separation = 20
+__meta__ = {
+
+}
+
+[node name="ServerControl" parent="Box" instance=ExtResource( 1 )]
+anchor_right = 0.0
+anchor_bottom = 0.0
+margin_right = 502.0
+margin_bottom = 600.0
+size_flags_horizontal = 3
+
+[node name="VBoxContainer" type="VBoxContainer" parent="Box"]
+margin_left = 522.0
+margin_right = 1024.0
+margin_bottom = 600.0
+size_flags_horizontal = 3
+__meta__ = {
+
+}
+
+[node name="Client" parent="Box/VBoxContainer" instance=ExtResource( 2 )]
+anchor_right = 0.0
+anchor_bottom = 0.0
+margin_right = 502.0
+margin_bottom = 197.0
+size_flags_horizontal = 3
+size_flags_vertical = 3
+
+[node name="Client2" parent="Box/VBoxContainer" instance=ExtResource( 2 )]
+anchor_right = 0.0
+anchor_bottom = 0.0
+margin_top = 201.0
+margin_right = 502.0
+margin_bottom = 398.0
+size_flags_horizontal = 3
+size_flags_vertical = 3
+
+[node name="Client3" parent="Box/VBoxContainer" instance=ExtResource( 2 )]
+anchor_right = 0.0
+anchor_bottom = 0.0
+margin_top = 402.0
+margin_right = 502.0
+margin_bottom = 600.0
+size_flags_horizontal = 3
+size_flags_vertical = 3

BIN
networking/websocket_chat/icon.png


+ 34 - 0
networking/websocket_chat/icon.png.import

@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="StreamTexture"
+path="res://.import/icon.png-487276ed1e3a0c39cad0279d744ee560.stex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://icon.png"
+dest_files=[ "res://.import/icon.png-487276ed1e3a0c39cad0279d744ee560.stex" ]
+
+[params]
+
+compress/mode=0
+compress/lossy_quality=0.7
+compress/hdr_mode=0
+compress/bptc_ldr=0
+compress/normal_map=0
+flags/repeat=0
+flags/filter=true
+flags/mipmaps=false
+flags/anisotropic=false
+flags/srgb=2
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/HDR_as_SRGB=false
+process/invert_color=false
+stream=false
+size_limit=0
+detect_3d=true
+svg/scale=1.0

+ 28 - 0
networking/websocket_chat/project.godot

@@ -0,0 +1,28 @@
+; Engine configuration file.
+; It's best edited using the editor UI and not directly,
+; since the parameters that go here are not all obvious.
+;
+; Format:
+;   [section] ; section goes between []
+;   param=value ; assign values to parameters
+
+config_version=4
+
+_global_script_classes=[  ]
+_global_script_class_icons={
+
+}
+
+[application]
+
+config/name="Websocket Chat Demo"
+run/main_scene="res://combo/combo.tscn"
+config/icon="res://icon.png"
+
+[autoload]
+
+Utils="*res://utils.gd"
+
+[gdnative]
+
+singletons=[  ]

+ 74 - 0
networking/websocket_chat/server/server.gd

@@ -0,0 +1,74 @@
+extends Node
+
+onready var _log_dest = get_parent().get_node("Panel/VBoxContainer/RichTextLabel")
+
+var _server = WebSocketServer.new()
+var _clients = {}
+var _write_mode = WebSocketPeer.WRITE_MODE_BINARY
+var _use_multiplayer = true
+var last_connected_client = 0
+
+func _init():
+	_server.connect("client_connected", self, "_client_connected")
+	_server.connect("client_disconnected", self, "_client_disconnected")
+	_server.connect("client_close_request", self, "_client_close_request")
+	_server.connect("data_received", self, "_client_receive")
+
+	_server.connect("peer_packet", self, "_client_receive")
+	_server.connect("peer_connected", self, "_client_connected", ["multiplayer_protocol"])
+	_server.connect("peer_disconnected", self, "_client_disconnected")
+
+func _exit_tree():
+	_clients.clear()
+	_server.stop()
+
+func _process(delta):
+	if _server.is_listening():
+		_server.poll()
+
+func _client_close_request(id, code, reason):
+	print(reason == "Bye bye!")
+	Utils._log(_log_dest, "Client %s close code: %d, reason: %s" % [id, code, reason])
+
+func _client_connected(id, protocol):
+	_clients[id] = _server.get_peer(id)
+	_clients[id].set_write_mode(_write_mode)
+	last_connected_client = id
+	Utils._log(_log_dest, "%s: Client connected with protocol %s" % [id, protocol])
+
+func _client_disconnected(id, clean = true):
+	Utils._log(_log_dest, "Client %s disconnected. Was clean: %s" % [id, clean])
+	if _clients.has(id):
+		_clients.erase(id)
+
+func _client_receive(id):
+	if _use_multiplayer:
+		var peer_id = _server.get_packet_peer()
+		var packet = _server.get_packet()
+		Utils._log(_log_dest, "MPAPI: From %s data: %s" % [peer_id, Utils.decode_data(packet, false)])
+	else:
+		var packet = _server.get_peer(id).get_packet()
+		var is_string = _server.get_peer(id).was_string_packet()
+		Utils._log(_log_dest, "Data from %s BINARY: %s: %s" % [id, not is_string, Utils.decode_data(packet, is_string)])
+
+func send_data(data, dest):
+	if _use_multiplayer:
+		_server.set_target_peer(dest)
+		_server.put_packet(Utils.encode_data(data, _write_mode))
+	else:
+		for id in _clients:
+			_server.get_peer(id).put_packet(Utils.encode_data(data, _write_mode))
+
+func listen(port, supported_protocols, multiplayer):
+	_use_multiplayer = multiplayer
+	if _use_multiplayer:
+		set_write_mode(WebSocketPeer.WRITE_MODE_BINARY)
+	return _server.listen(port, supported_protocols, multiplayer)
+
+func stop():
+	_server.stop()
+
+func set_write_mode(mode):
+	_write_mode = mode
+	for c in _clients:
+		_clients[c].set_write_mode(_write_mode)

+ 129 - 0
networking/websocket_chat/server/server.tscn

@@ -0,0 +1,129 @@
+[gd_scene load_steps=3 format=2]
+
+[ext_resource path="res://server/server_ui.gd" type="Script" id=1]
+[ext_resource path="res://server/server.gd" type="Script" id=2]
+
+[node name="ServerControl" type="Control"]
+anchor_right = 1.0
+anchor_bottom = 1.0
+script = ExtResource( 1 )
+__meta__ = {
+
+}
+
+[node name="Server" type="Node" parent="."]
+script = ExtResource( 2 )
+__meta__ = {
+
+}
+
+[node name="Panel" type="Panel" parent="."]
+anchor_right = 1.0
+anchor_bottom = 1.0
+__meta__ = {
+
+}
+
+[node name="VBoxContainer" type="VBoxContainer" parent="Panel"]
+anchor_right = 1.0
+anchor_bottom = 1.0
+__meta__ = {
+
+}
+
+[node name="HBoxContainer" type="HBoxContainer" parent="Panel/VBoxContainer"]
+margin_right = 1024.0
+margin_bottom = 24.0
+__meta__ = {
+
+}
+
+[node name="Port" type="SpinBox" parent="Panel/VBoxContainer/HBoxContainer"]
+margin_right = 74.0
+margin_bottom = 24.0
+min_value = 1.0
+max_value = 65535.0
+value = 8000.0
+__meta__ = {
+
+}
+
+[node name="Listen" type="Button" parent="Panel/VBoxContainer/HBoxContainer"]
+margin_left = 78.0
+margin_right = 129.0
+margin_bottom = 24.0
+toggle_mode = true
+text = "Listen"
+__meta__ = {
+
+}
+
+[node name="HBoxContainer2" type="HBoxContainer" parent="Panel/VBoxContainer"]
+margin_top = 28.0
+margin_right = 1024.0
+margin_bottom = 52.0
+__meta__ = {
+
+}
+
+[node name="WriteMode" type="OptionButton" parent="Panel/VBoxContainer/HBoxContainer2"]
+margin_right = 41.0
+margin_bottom = 24.0
+__meta__ = {
+
+}
+
+[node name="MPAPI" type="CheckBox" parent="Panel/VBoxContainer/HBoxContainer2"]
+margin_left = 45.0
+margin_right = 171.0
+margin_bottom = 24.0
+pressed = true
+text = "Multiplayer API"
+__meta__ = {
+
+}
+
+[node name="Destination" type="OptionButton" parent="Panel/VBoxContainer/HBoxContainer2"]
+margin_left = 175.0
+margin_right = 216.0
+margin_bottom = 24.0
+__meta__ = {
+
+}
+
+[node name="HBoxContainer3" type="HBoxContainer" parent="Panel/VBoxContainer"]
+margin_top = 56.0
+margin_right = 1024.0
+margin_bottom = 80.0
+__meta__ = {
+
+}
+
+[node name="LineEdit" type="LineEdit" parent="Panel/VBoxContainer/HBoxContainer3"]
+margin_right = 977.0
+margin_bottom = 24.0
+size_flags_horizontal = 3
+__meta__ = {
+
+}
+
+[node name="Send" type="Button" parent="Panel/VBoxContainer/HBoxContainer3"]
+margin_left = 981.0
+margin_right = 1024.0
+margin_bottom = 24.0
+text = "Send"
+__meta__ = {
+
+}
+
+[node name="RichTextLabel" type="RichTextLabel" parent="Panel/VBoxContainer"]
+margin_top = 84.0
+margin_right = 1024.0
+margin_bottom = 600.0
+size_flags_vertical = 3
+__meta__ = {
+
+}
+[connection signal="toggled" from="Panel/VBoxContainer/HBoxContainer/Listen" to="." method="_on_Listen_toggled"]
+[connection signal="item_selected" from="Panel/VBoxContainer/HBoxContainer2/WriteMode" to="." method="_on_WriteMode_item_selected"]
+[connection signal="pressed" from="Panel/VBoxContainer/HBoxContainer3/Send" to="." method="_on_Send_pressed"]

+ 67 - 0
networking/websocket_chat/server/server_ui.gd

@@ -0,0 +1,67 @@
+extends Control
+
+onready var _server = get_node("Server")
+onready var _port = get_node("Panel/VBoxContainer/HBoxContainer/Port")
+onready var _line_edit = get_node("Panel/VBoxContainer/HBoxContainer3/LineEdit")
+onready var _write_mode = get_node("Panel/VBoxContainer/HBoxContainer2/WriteMode")
+onready var _log_dest = get_node("Panel/VBoxContainer/RichTextLabel")
+onready var _multiplayer = get_node("Panel/VBoxContainer/HBoxContainer2/MPAPI")
+onready var _destination = get_node("Panel/VBoxContainer/HBoxContainer2/Destination")
+
+func _ready():
+	_write_mode.clear()
+	_write_mode.add_item("BINARY")
+	_write_mode.set_item_metadata(0, WebSocketPeer.WRITE_MODE_BINARY)
+	_write_mode.add_item("TEXT")
+	_write_mode.set_item_metadata(1, WebSocketPeer.WRITE_MODE_TEXT)
+	_write_mode.select(0)
+
+	_destination.add_item("Broadcast")
+	_destination.set_item_metadata(0, 0)
+	_destination.add_item("Last connected")
+	_destination.set_item_metadata(1, 1)
+	_destination.add_item("All But last connected")
+	_destination.set_item_metadata(2, -1)
+	_destination.select(0)
+
+func _on_Listen_toggled( pressed ):
+	if pressed:
+		var use_multiplayer = _multiplayer.pressed
+		_multiplayer.disabled = true
+		var supported_protocols = PoolStringArray(["my-protocol", "binary"])
+		var port = int(_port.value)
+		if use_multiplayer:
+			_write_mode.disabled = true
+			_write_mode.select(0)
+		else:
+			_destination.disabled = true
+			_destination.select(0)
+		if _server.listen(port, supported_protocols, use_multiplayer) == OK:
+			Utils._log(_log_dest, "Listing on port %s" % port)
+			if not use_multiplayer:
+				Utils._log(_log_dest, "Supported protocols: %s" % supported_protocols)
+		else:
+			Utils._log(_log_dest, "Error listening on port %s" % port)
+	else:
+		_server.stop()
+		_multiplayer.disabled = false
+		_write_mode.disabled = false
+		_destination.disabled = false
+		Utils._log(_log_dest, "Server stopped")
+
+func _on_Send_pressed():
+	if _line_edit.text == "":
+		return
+
+	var dest = _destination.get_selected_metadata()
+	if dest > 0:
+		dest = _server.last_connected_client
+	elif dest < 0:
+		dest = -_server.last_connected_client
+
+	Utils._log(_log_dest, "Sending data %s to %s" % [_line_edit.text, dest])
+	_server.send_data(_line_edit.text, dest)
+	_line_edit.text = ""
+
+func _on_WriteMode_item_selected( ID ):
+	_server.set_write_mode(_write_mode.get_selected_metadata())

+ 11 - 0
networking/websocket_chat/utils.gd

@@ -0,0 +1,11 @@
+extends Node
+
+func encode_data(data, mode):
+	return data.to_utf8() if mode == WebSocketPeer.WRITE_MODE_TEXT else var2bytes(data)
+
+func decode_data(data, is_string):
+	return data.get_string_from_utf8() if is_string else bytes2var(data)
+
+func _log(node, msg):
+	print(msg)
+	node.add_text(str(msg) + "\n")

+ 21 - 0
networking/websocket_minimal/Main.tscn

@@ -0,0 +1,21 @@
+[gd_scene load_steps=3 format=2]
+
+[ext_resource path="res://server.gd" type="Script" id=1]
+[ext_resource path="res://client.gd" type="Script" id=2]
+
+[node name="Main" type="Node"]
+__meta__ = {
+
+}
+
+[node name="Server" type="Node" parent="."]
+script = ExtResource( 1 )
+__meta__ = {
+
+}
+
+[node name="Client" type="Node" parent="."]
+script = ExtResource( 2 )
+__meta__ = {
+
+}

+ 51 - 0
networking/websocket_minimal/client.gd

@@ -0,0 +1,51 @@
+extends Node
+
+# The URL we will connect to
+export var websocket_url = "ws://localhost:9080"
+
+# Our WebSocketClient instance
+var _client = WebSocketClient.new()
+
+func _ready():
+	# Connect base signals to get notified of connection open, close, and errors.
+	_client.connect("connection_closed", self, "_closed")
+	_client.connect("connection_error", self, "_closed")
+	_client.connect("connection_established", self, "_connected")
+	# This signal is emitted when not using the Multiplayer API every time
+	# a full packet is received.
+	# Alternatively, you could check get_peer(1).get_available_packets() in a loop.
+	_client.connect("data_received", self, "_on_data")
+
+	# Initiate connection to the given URL.
+	var err = _client.connect_to_url(websocket_url)
+	if err != OK:
+		print("Unable to connect")
+		set_process(false)
+
+func _closed(was_clean = false):
+	# was_clean will tell you if the disconnection was correctly notified
+	# by the remote peer before closing the socket.
+	print("Closed, clean: ", was_clean)
+	set_process(false)
+
+func _connected(proto = ""):
+	# This is called on connection, "proto" will be the selected WebSocket
+	# sub-protocol (which is optional)
+	print("Connected with protocol: ", proto)
+	# You MUST always use get_peer(1).put_packet to send data to server,
+	# and not put_packet directly when not using the MultiplayerAPI.
+	_client.get_peer(1).put_packet("Test packet".to_utf8())
+
+func _on_data():
+	# Print the received packet, you MUST always use get_peer(1).get_packet
+	# to receive data from server, and not get_packet directly when not
+	# using the MultiplayerAPI.
+	print("Got data from server: ", _client.get_peer(1).get_packet().get_string_from_utf8())
+
+func _process(delta):
+	# Call this in _process or _physics_process. Data transfer, and signals
+	# emission will only happen when calling this function.
+	_client.poll()
+
+func _exit_tree():
+	_client.disconnect_from_host()

+ 19 - 0
networking/websocket_minimal/project.godot

@@ -0,0 +1,19 @@
+; Engine configuration file.
+; It's best edited using the editor UI and not directly,
+; since the parameters that go here are not all obvious.
+;
+; Format:
+;   [section] ; section goes between []
+;   param=value ; assign values to parameters
+
+config_version=4
+
+_global_script_classes=[  ]
+_global_script_class_icons={
+
+}
+
+[application]
+
+config/name="WebSocket Minimal Demo"
+run/main_scene="res://Main.tscn"

+ 54 - 0
networking/websocket_minimal/server.gd

@@ -0,0 +1,54 @@
+extends Node
+
+# The port we will listen to
+const PORT = 9080
+# Our WebSocketServer instance
+var _server = WebSocketServer.new()
+
+func _ready():
+	# Connect base signals to get notified of new client connections,
+	# disconnections, and disconnect requests.
+	_server.connect("client_connected", self, "_connected")
+	_server.connect("client_disconnected", self, "_disconnected")
+	_server.connect("client_close_request", self, "_close_request")
+	# This signal is emitted when not using the Multiplayer API every time a
+	# full packet is received.
+	# Alternatively, you could check get_peer(PEER_ID).get_available_packets()
+	# in a loop for each connected peer.
+	_server.connect("data_received", self, "_on_data")
+	# Start listening on the given port.
+	var err = _server.listen(PORT)
+	if err != OK:
+		print("Unable to start server")
+		set_process(false)
+
+func _connected(id, proto):
+	# This is called when a new peer connects, "id" will be the assigned peer id,
+	# "proto" will be the selected WebSocket sub-protocol (which is optional)
+	print("Client %d connected with protocol: %s" % [id, proto])
+
+func _close_request(id, code, reason):
+	# This is called when a client notifies that it wishes to close the connection,
+	# providing a reason string and close code.
+	print("Client %d disconnecting with code: %d, reason: %s" % [id, code, reason])
+
+func _disconnected(id, was_clean = false):
+	# This is called when a client disconnects, "id" will be the one of the
+	# disconnecting client, "was_clean" will tell you if the disconnection
+	# was correctly notified by the remote peer before closing the socket.
+	print("Client %d disconnected, clean: %s" % [id, str(was_clean)])
+
+func _on_data(id):
+	# Print the received packet, you MUST always use get_peer(id).get_packet to receive data,
+	# and not get_packet directly when not using the MultiplayerAPI.
+	var pkt = _server.get_peer(id).get_packet()
+	print("Got data from client %d: %s ... echoing" % [id, pkt.get_string_from_utf8()])
+	_server.get_peer(id).put_packet(pkt)
+
+func _process(delta):
+	# Call this in _process or _physics_process.
+	# Data transfer, and signals emission will only happen when calling this function.
+	_server.poll()
+
+func _exit_tree():
+	_server.stop()

+ 3 - 0
networking/websocket_multiplayer/.gitignore

@@ -0,0 +1,3 @@
+.import/*
+*.import
+export_presets.cfg

+ 20 - 0
networking/websocket_multiplayer/default_env.tres

@@ -0,0 +1,20 @@
+[gd_resource type="Environment" load_steps=2 format=2]
+
+[sub_resource type="ProceduralSky" id=1]
+sky_top_color = Color( 0.0470588, 0.454902, 0.976471, 1 )
+sky_horizon_color = Color( 0.556863, 0.823529, 0.909804, 1 )
+sky_curve = 0.25
+ground_bottom_color = Color( 0.101961, 0.145098, 0.188235, 1 )
+ground_horizon_color = Color( 0.482353, 0.788235, 0.952941, 1 )
+ground_curve = 0.01
+sun_energy = 16.0
+__meta__ = {
+
+}
+
+[resource]
+background_mode = 2
+background_sky = SubResource( 1 )
+__meta__ = {
+
+}

BIN
networking/websocket_multiplayer/icon.png


BIN
networking/websocket_multiplayer/img/crown.png


+ 24 - 0
networking/websocket_multiplayer/project.godot

@@ -0,0 +1,24 @@
+; Engine configuration file.
+; It's best edited using the editor UI and not directly,
+; since the parameters that go here are not all obvious.
+;
+; Format:
+;   [section] ; section goes between []
+;   param=value ; assign values to parameters
+
+config_version=4
+
+_global_script_classes=[  ]
+_global_script_class_icons={
+
+}
+
+[application]
+
+config/name="Websocket Multiplayer Demo"
+run/main_scene="res://scene/main.tscn"
+config/icon="res://icon.png"
+
+[rendering]
+
+environment/default_environment="res://default_env.tres"

+ 68 - 0
networking/websocket_multiplayer/scene/game.tscn

@@ -0,0 +1,68 @@
+[gd_scene load_steps=2 format=2]
+
+[ext_resource path="res://script/game.gd" type="Script" id=1]
+
+[node name="Game" type="Control"]
+anchor_right = 1.0
+anchor_bottom = 1.0
+mouse_filter = 1
+size_flags_horizontal = 3
+size_flags_vertical = 3
+script = ExtResource( 1 )
+__meta__ = {
+"_edit_use_anchors_": false
+}
+
+[node name="HBoxContainer" type="HBoxContainer" parent="."]
+anchor_right = 1.0
+anchor_bottom = 1.0
+__meta__ = {
+
+}
+
+[node name="RichTextLabel" type="RichTextLabel" parent="HBoxContainer"]
+margin_right = 510.0
+margin_bottom = 600.0
+size_flags_horizontal = 3
+__meta__ = {
+
+}
+
+[node name="VBoxContainer" type="VBoxContainer" parent="HBoxContainer"]
+margin_left = 514.0
+margin_right = 1024.0
+margin_bottom = 600.0
+size_flags_horizontal = 3
+__meta__ = {
+
+}
+
+[node name="Label" type="Label" parent="HBoxContainer/VBoxContainer"]
+margin_right = 510.0
+margin_bottom = 14.0
+text = "Players:"
+__meta__ = {
+
+}
+
+[node name="ItemList" type="ItemList" parent="HBoxContainer/VBoxContainer"]
+margin_top = 18.0
+margin_right = 510.0
+margin_bottom = 576.0
+size_flags_horizontal = 3
+size_flags_vertical = 3
+same_column_width = true
+__meta__ = {
+
+}
+
+[node name="Action" type="Button" parent="HBoxContainer/VBoxContainer"]
+margin_top = 580.0
+margin_right = 510.0
+margin_bottom = 600.0
+disabled = true
+text = "Do Action!"
+__meta__ = {
+
+}
+[connection signal="pressed" from="HBoxContainer/VBoxContainer/Action" to="." method="_on_Action_pressed"]

+ 155 - 0
networking/websocket_multiplayer/scene/main.tscn

@@ -0,0 +1,155 @@
+[gd_scene load_steps=3 format=2]
+
+[ext_resource path="res://script/main.gd" type="Script" id=1]
+[ext_resource path="res://scene/game.tscn" type="PackedScene" id=2]
+
+[node name="Control" type="Control"]
+anchor_right = 1.0
+anchor_bottom = 1.0
+script = ExtResource( 1 )
+__meta__ = {
+
+}
+
+[node name="Panel" type="Panel" parent="."]
+anchor_right = 1.0
+anchor_bottom = 1.0
+__meta__ = {
+
+}
+
+[node name="VBoxContainer" type="VBoxContainer" parent="Panel"]
+anchor_right = 1.0
+anchor_bottom = 1.0
+margin_left = 20.0
+margin_top = 20.0
+margin_right = -20.0
+margin_bottom = -20.0
+__meta__ = {
+
+}
+
+[node name="HBoxContainer" type="HBoxContainer" parent="Panel/VBoxContainer"]
+margin_right = 984.0
+margin_bottom = 24.0
+__meta__ = {
+
+}
+
+[node name="Label" type="Label" parent="Panel/VBoxContainer/HBoxContainer"]
+margin_top = 5.0
+margin_right = 326.0
+margin_bottom = 19.0
+size_flags_horizontal = 3
+text = "Name"
+__meta__ = {
+
+}
+
+[node name="NameEdit" type="LineEdit" parent="Panel/VBoxContainer/HBoxContainer"]
+margin_left = 330.0
+margin_right = 984.0
+margin_bottom = 24.0
+size_flags_horizontal = 3
+size_flags_stretch_ratio = 2.0
+text = "A Godot User"
+__meta__ = {
+
+}
+
+[node name="HBoxContainer2" type="HBoxContainer" parent="Panel/VBoxContainer"]
+margin_top = 28.0
+margin_right = 984.0
+margin_bottom = 52.0
+__meta__ = {
+
+}
+
+[node name="HBoxContainer" type="HBoxContainer" parent="Panel/VBoxContainer/HBoxContainer2"]
+margin_right = 326.0
+margin_bottom = 24.0
+size_flags_horizontal = 3
+__meta__ = {
+
+}
+
+[node name="Host" type="Button" parent="Panel/VBoxContainer/HBoxContainer2/HBoxContainer"]
+margin_right = 42.0
+margin_bottom = 24.0
+text = "Host"
+__meta__ = {
+
+}
+
+[node name="Control" type="Control" parent="Panel/VBoxContainer/HBoxContainer2/HBoxContainer"]
+margin_left = 46.0
+margin_right = 241.0
+margin_bottom = 24.0
+size_flags_horizontal = 3
+__meta__ = {
+
+}
+
+[node name="Connect" type="Button" parent="Panel/VBoxContainer/HBoxContainer2/HBoxContainer"]
+margin_left = 245.0
+margin_right = 326.0
+margin_bottom = 24.0
+text = "Connect to"
+__meta__ = {
+
+}
+
+[node name="Disconnect" type="Button" parent="Panel/VBoxContainer/HBoxContainer2/HBoxContainer"]
+visible = false
+margin_left = 68.0
+margin_right = 152.0
+margin_bottom = 24.0
+text = "Disconnect"
+__meta__ = {
+
+}
+
+[node name="Hostname" type="LineEdit" parent="Panel/VBoxContainer/HBoxContainer2"]
+margin_left = 330.0
+margin_right = 984.0
+margin_bottom = 24.0
+size_flags_horizontal = 3
+size_flags_stretch_ratio = 2.0
+text = "localhost"
+placeholder_text = "localhost"
+__meta__ = {
+
+}
+
+[node name="Control" type="Control" parent="Panel/VBoxContainer"]
+margin_top = 56.0
+margin_right = 984.0
+margin_bottom = 76.0
+rect_min_size = Vector2( 0, 20 )
+__meta__ = {
+
+}
+
+[node name="Game" parent="Panel/VBoxContainer" instance=ExtResource( 2 )]
+anchor_right = 0.0
+anchor_bottom = 0.0
+margin_top = 80.0
+margin_right = 984.0
+margin_bottom = 560.0
+
+[node name="AcceptDialog" type="AcceptDialog" parent="."]
+anchor_left = 0.5
+anchor_top = 0.5
+anchor_right = 0.5
+anchor_bottom = 0.5
+margin_left = -200.0
+margin_top = -100.0
+margin_right = 200.0
+margin_bottom = 100.0
+dialog_text = "Connection closed"
+__meta__ = {
+
+}
+[connection signal="pressed" from="Panel/VBoxContainer/HBoxContainer2/HBoxContainer/Host" to="." method="_on_Host_pressed"]
+[connection signal="pressed" from="Panel/VBoxContainer/HBoxContainer2/HBoxContainer/Connect" to="." method="_on_Connect_pressed"]
+[connection signal="pressed" from="Panel/VBoxContainer/HBoxContainer2/HBoxContainer/Disconnect" to="." method="_on_Disconnect_pressed"]

+ 104 - 0
networking/websocket_multiplayer/script/game.gd

@@ -0,0 +1,104 @@
+extends Control
+
+const _crown = preload("res://img/crown.png")
+
+onready var _list = $HBoxContainer/VBoxContainer/ItemList
+onready var _action = $HBoxContainer/VBoxContainer/Action
+
+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)
+
+sync 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))
+		return
+	do_action(action)
+	next_turn()
+
+sync func do_action(action):
+	var name = _list.get_item_text(_turn)
+	var val = randi() % 100
+	rpc("_log", "%s: %ss %d" % [name, action, val])
+
+sync func set_turn(turn):
+	_turn = turn
+	if turn >= _players.size():
+		return
+	for i in range(0, _players.size()):
+		if i == turn:
+			_list.set_item_icon(i, _crown)
+		else:
+			_list.set_item_icon(i, null)
+	_action.disabled = _players[turn] != get_tree().get_network_unique_id()
+
+sync func del_player(id):
+	var pos = _players.find(id)
+	if pos == -1:
+		return
+	_players.remove(pos)
+	_list.remove_item(pos)
+	if _turn > pos:
+		_turn -= 1
+	if get_tree().is_network_server():
+		rpc("set_turn", _turn)
+
+sync func add_player(id, name=""):
+	_players.append(id)
+	if name == "":
+		_list.add_item("... connecting ...", null, false)
+	else:
+		_list.add_item(name, null, false)
+
+func get_player_name(pos):
+	if pos < _list.get_item_count():
+		return _list.get_item_text(pos)
+	else:
+		return "Error!"
+
+func next_turn():
+	_turn += 1
+	if _turn >= _players.size():
+		_turn = 0
+	rpc("set_turn", _turn)
+
+func start():
+	set_turn(0)
+
+func stop():
+	_players.clear()
+	_list.clear()
+	_turn = 0
+	_action.disabled = true
+
+func on_peer_add(id):
+	if not get_tree().is_network_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)
+
+func on_peer_del(id):
+	if not get_tree().is_network_server():
+		return
+	rpc("del_player", id)
+
+sync func _log(what):
+	$HBoxContainer/RichTextLabel.add_text(what + "\n")
+
+func _on_Action_pressed():
+	if get_tree().is_network_server():
+		do_action("roll")
+		next_turn()
+	else:
+		rpc_id(1, "request_action", "roll")

+ 73 - 0
networking/websocket_multiplayer/script/main.gd

@@ -0,0 +1,73 @@
+extends Control
+
+const DEF_PORT = 8080
+const PROTO_NAME = "ludus"
+
+onready var _host_btn = $Panel/VBoxContainer/HBoxContainer2/HBoxContainer/Host
+onready var _connect_btn = $Panel/VBoxContainer/HBoxContainer2/HBoxContainer/Connect
+onready var _disconnect_btn = $Panel/VBoxContainer/HBoxContainer2/HBoxContainer/Disconnect
+onready var _name_edit = $Panel/VBoxContainer/HBoxContainer/NameEdit
+onready var _host_edit = $Panel/VBoxContainer/HBoxContainer2/Hostname
+onready var _game = $Panel/VBoxContainer/Game
+
+func _ready():
+	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
+
+func start_game():
+	_host_btn.disabled = true
+	_name_edit.editable = false
+	_host_edit.editable = false
+	_connect_btn.hide()
+	_disconnect_btn.show()
+	_game.start()
+
+func stop_game():
+	_host_btn.disabled = false
+	_name_edit.editable = true
+	_host_edit.editable = true
+	_disconnect_btn.hide()
+	_connect_btn.show()
+	_game.stop()
+
+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)
+
+func _connected():
+	_game.rpc("set_player_name", _name_edit.text)
+
+func _peer_connected(id):
+	_game.on_peer_add(id)
+
+func _peer_disconnected(id):
+	_game.on_peer_del(id)
+
+func _on_Host_pressed():
+	var host = WebSocketServer.new()
+	host.listen(DEF_PORT, PoolStringArray(["ludus"]), true)
+	get_tree().connect("server_disconnected", self, "_close_network")
+	get_tree().set_network_peer(host)
+	_game.add_player(1, _name_edit.text)
+	start_game()
+
+func _on_Disconnect_pressed():
+	_close_network()
+
+func _on_Connect_pressed():
+	var host = WebSocketClient.new()
+	host.connect_to_url("ws://" + _host_edit.text + ":" + str(DEF_PORT), PoolStringArray([PROTO_NAME]), true)
+	get_tree().connect("connection_failed", self, "_close_network")
+	get_tree().connect("connected_to_server", self, "_connected")
+	get_tree().set_network_peer(host)
+	start_game()