From b915552a64b6f84a636eb61ddccd96e407d03109 Mon Sep 17 00:00:00 2001 From: Daniel Flanagan Date: Mon, 15 Nov 2021 15:02:55 -0600 Subject: [PATCH] Initial commit --- .gitignore | 1 + client_ui.gd | 64 +++++++++++++++++++++++ client_ui.tscn | 107 ++++++++++++++++++++++++++++++++++++++ default_env.tres | 2 + main.gd | 14 +++++ main.tscn | 98 +++++++++++++++++++++++++++++++++++ multiplayer_client.gd | 86 +++++++++++++++++++++++++++++++ project.godot | 5 ++ readme.md | 4 ++ server.ts | 109 +++++++++++++++++++++------------------ ws_webrtc_client.gd | 116 ++++++++++++++++++++++++++++++++++++++++++ 11 files changed, 557 insertions(+), 49 deletions(-) create mode 100644 client_ui.gd create mode 100644 client_ui.tscn create mode 100644 main.gd create mode 100644 main.tscn create mode 100644 multiplayer_client.gd create mode 100644 ws_webrtc_client.gd diff --git a/.gitignore b/.gitignore index 3922fbd..72d9842 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ export_presets.cfg *.translation .mono data_*/ +/webrtc/ diff --git a/client_ui.gd b/client_ui.gd new file mode 100644 index 0000000..5457966 --- /dev/null +++ b/client_ui.gd @@ -0,0 +1,64 @@ +extends Control + +onready var client = $Client + +func _ready(): + client.connect("lobby_joined", self, "_lobby_joined") + client.connect("lobby_sealed", self, "_lobby_sealed") + client.connect("connected", self, "_connected") + client.connect("disconnected", self, "_disconnected") + client.rtc_mp.connect("peer_connected", self, "_mp_peer_connected") + client.rtc_mp.connect("peer_disconnected", self, "_mp_peer_disconnected") + client.rtc_mp.connect("server_disconnected", self, "_mp_server_disconnect") + client.rtc_mp.connect("connection_succeeded", self, "_mp_connected") + +func _process(delta): + client.rtc_mp.poll() + while client.rtc_mp.get_available_packet_count() > 0: + _log(client.rtc_mp.get_packet().get_string_from_utf8()) + +func _connected(id): + _log("Signaling server connected with ID: %d" % id) + +func _disconnected(): + _log("Signaling server disconnected: %d - %s" % [client.code, client.reason]) + +func _lobby_joined(lobby): + _log("Joined lobby %s" % lobby) + +func _lobby_sealed(): + _log("Lobby has been sealed") + +func _mp_connected(): + _log("Multiplayer is connected (I am %d)" % client.rtc_mp.get_unique_id()) + +func _mp_server_disconnect(): + _log("Multiplayer is disconnected (I am %d)" % client.rtc_mp.get_unique_id()) + +func _mp_peer_connected(id: int): + _log("Multiplayer peer %d connected" % id) + +func _mp_peer_disconnected(id: int): + _log("Multiplayer peer %d disconnected" % id) + +func _log(msg): + print(msg) + $VBoxContainer/TextEdit.text += str(msg) + "\n" + +func ping(): + _log(client.rtc_mp.put_packet("ping".to_utf8())) + +func _on_Peers_pressed(): + var d = client.rtc_mp.get_peers() + _log(d) + for k in d: + _log(client.rtc_mp.get_peer(k)) + +func start(): + client.start($VBoxContainer/Connect/Host.text, $VBoxContainer/Connect/RoomSecret.text) + +func _on_Seal_pressed(): + client.seal_lobby() + +func stop(): + client.stop() diff --git a/client_ui.tscn b/client_ui.tscn new file mode 100644 index 0000000..6e1c31b --- /dev/null +++ b/client_ui.tscn @@ -0,0 +1,107 @@ +[gd_scene load_steps=3 format=2] + +[ext_resource path="res://client_ui.gd" type="Script" id=1] +[ext_resource path="res://multiplayer_client.gd" type="Script" id=2] + +[node name="ClientUI" type="Control"] +margin_right = 1024.0 +margin_bottom = 600.0 +size_flags_horizontal = 3 +size_flags_vertical = 3 +script = ExtResource( 1 ) +__meta__ = { +"_edit_use_anchors_": true +} + +[node name="Client" type="Node" parent="."] +script = ExtResource( 2 ) + +[node name="VBoxContainer" type="VBoxContainer" parent="."] +anchor_right = 1.0 +anchor_bottom = 1.0 +custom_constants/separation = 8 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="Connect" type="HBoxContainer" parent="VBoxContainer"] +margin_right = 1024.0 +margin_bottom = 24.0 + +[node name="Label" type="Label" parent="VBoxContainer/Connect"] +margin_top = 5.0 +margin_right = 73.0 +margin_bottom = 19.0 +text = "Connect to:" + +[node name="Host" type="LineEdit" parent="VBoxContainer/Connect"] +margin_left = 77.0 +margin_right = 921.0 +margin_bottom = 24.0 +size_flags_horizontal = 3 +text = "ws://localhost:9080" + +[node name="Room" type="Label" parent="VBoxContainer/Connect"] +margin_left = 925.0 +margin_right = 962.0 +margin_bottom = 24.0 +size_flags_vertical = 5 +text = "Room" +valign = 1 + +[node name="RoomSecret" type="LineEdit" parent="VBoxContainer/Connect"] +margin_left = 966.0 +margin_right = 1024.0 +margin_bottom = 24.0 +placeholder_text = "secret" + +[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"] +margin_top = 32.0 +margin_right = 1024.0 +margin_bottom = 52.0 +custom_constants/separation = 10 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="Start" type="Button" parent="VBoxContainer/HBoxContainer"] +margin_right = 41.0 +margin_bottom = 20.0 +text = "Start" + +[node name="Stop" type="Button" parent="VBoxContainer/HBoxContainer"] +margin_left = 51.0 +margin_right = 91.0 +margin_bottom = 20.0 +text = "Stop" + +[node name="Seal" type="Button" parent="VBoxContainer/HBoxContainer"] +margin_left = 101.0 +margin_right = 139.0 +margin_bottom = 20.0 +text = "Seal" + +[node name="Ping" type="Button" parent="VBoxContainer/HBoxContainer"] +margin_left = 149.0 +margin_right = 188.0 +margin_bottom = 20.0 +text = "Ping" + +[node name="Peers" type="Button" parent="VBoxContainer/HBoxContainer"] +margin_left = 198.0 +margin_right = 280.0 +margin_bottom = 20.0 +text = "Print peers" + +[node name="TextEdit" type="TextEdit" parent="VBoxContainer"] +margin_top = 60.0 +margin_right = 1024.0 +margin_bottom = 600.0 +size_flags_vertical = 3 +readonly = true + +[connection signal="pressed" from="VBoxContainer/HBoxContainer/Start" to="." method="start"] +[connection signal="pressed" from="VBoxContainer/HBoxContainer/Stop" to="." method="stop"] +[connection signal="pressed" from="VBoxContainer/HBoxContainer/Seal" to="." method="_on_Seal_pressed"] +[connection signal="pressed" from="VBoxContainer/HBoxContainer/Ping" to="." method="ping"] +[connection signal="pressed" from="VBoxContainer/HBoxContainer/Peers" to="." method="_on_Peers_pressed"] diff --git a/default_env.tres b/default_env.tres index 98f26a7..20207a4 100644 --- a/default_env.tres +++ b/default_env.tres @@ -1,5 +1,7 @@ [gd_resource type="Environment" load_steps=2 format=2] + [sub_resource type="ProceduralSky" id=1] + [resource] background_mode = 2 background_sky = SubResource( 1 ) diff --git a/main.gd b/main.gd new file mode 100644 index 0000000..250cfab --- /dev/null +++ b/main.gd @@ -0,0 +1,14 @@ +extends Control + +func _ready(): + if OS.get_name() == "HTML5": + $VBoxContainer/Signaling.hide() + +func _on_listen_toggled(button_pressed): + if button_pressed: + $Server.listen(int($VBoxContainer/Signaling/Port.value)) + else: + $Server.stop() + +func _on_LinkButton_pressed(): + OS.shell_open("https://github.com/godotengine/webrtc-native/releases") diff --git a/main.tscn b/main.tscn new file mode 100644 index 0000000..f92cccd --- /dev/null +++ b/main.tscn @@ -0,0 +1,98 @@ +[gd_scene load_steps=3 format=2] + +[ext_resource path="res://main.gd" type="Script" id=1] +[ext_resource path="res://client_ui.tscn" type="PackedScene" id=2] + +[node name="Control" type="Control"] +anchor_left = 0.0136719 +anchor_top = 0.0166667 +anchor_right = 0.986328 +anchor_bottom = 0.983333 +margin_top = 4.32134e-07 +margin_bottom = -9.53674e-06 +script = ExtResource( 1 ) +__meta__ = { +"_edit_use_anchors_": true +} + +[node name="VBoxContainer" type="VBoxContainer" parent="."] +anchor_right = 1.0 +anchor_bottom = 1.0 +custom_constants/separation = 50 +__meta__ = { +"_edit_use_anchors_": true +} + +[node name="Signaling" type="HBoxContainer" parent="VBoxContainer"] +margin_right = 995.0 +margin_bottom = 24.0 + +[node name="Label" type="Label" parent="VBoxContainer/Signaling"] +margin_top = 5.0 +margin_right = 104.0 +margin_bottom = 19.0 +text = "Signaling server:" + +[node name="Port" type="SpinBox" parent="VBoxContainer/Signaling"] +margin_left = 108.0 +margin_right = 182.0 +margin_bottom = 24.0 +min_value = 1025.0 +max_value = 65535.0 +value = 9080.0 + +[node name="ListenButton" type="Button" parent="VBoxContainer/Signaling"] +margin_left = 186.0 +margin_right = 237.0 +margin_bottom = 24.0 +toggle_mode = true +text = "Listen" + +[node name="CenterContainer" type="CenterContainer" parent="VBoxContainer/Signaling"] +margin_left = 241.0 +margin_right = 995.0 +margin_bottom = 24.0 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="LinkButton" type="LinkButton" parent="VBoxContainer/Signaling/CenterContainer"] +margin_left = 104.0 +margin_top = 5.0 +margin_right = 650.0 +margin_bottom = 19.0 +text = "Make sure to download the GDNative WebRTC Plugin and place it in the project folder" + +[node name="Clients" type="GridContainer" parent="VBoxContainer"] +margin_top = 74.0 +margin_right = 995.0 +margin_bottom = 579.0 +size_flags_horizontal = 3 +size_flags_vertical = 3 +custom_constants/vseparation = 15 +custom_constants/hseparation = 15 +columns = 2 + +[node name="ClientUI" parent="VBoxContainer/Clients" instance=ExtResource( 2 )] +margin_right = 490.0 +margin_bottom = 245.0 + +[node name="ClientUI2" parent="VBoxContainer/Clients" instance=ExtResource( 2 )] +margin_left = 505.0 +margin_right = 995.0 +margin_bottom = 245.0 + +[node name="ClientUI3" parent="VBoxContainer/Clients" instance=ExtResource( 2 )] +margin_top = 260.0 +margin_right = 490.0 +margin_bottom = 505.0 + +[node name="ClientUI4" parent="VBoxContainer/Clients" instance=ExtResource( 2 )] +margin_left = 505.0 +margin_top = 260.0 +margin_right = 995.0 +margin_bottom = 505.0 + +[node name="Server" type="Node" parent="."] + +[connection signal="toggled" from="VBoxContainer/Signaling/ListenButton" to="." method="_on_listen_toggled"] +[connection signal="pressed" from="VBoxContainer/Signaling/CenterContainer/LinkButton" to="." method="_on_LinkButton_pressed"] diff --git a/multiplayer_client.gd b/multiplayer_client.gd new file mode 100644 index 0000000..7a01486 --- /dev/null +++ b/multiplayer_client.gd @@ -0,0 +1,86 @@ +extends "ws_webrtc_client.gd" + +var rtc_mp: WebRTCMultiplayer = WebRTCMultiplayer.new() +var sealed = false + +func _init(): + connect("connected", self, "connected") + connect("disconnected", self, "disconnected") + + connect("offer_received", self, "offer_received") + connect("answer_received", self, "answer_received") + connect("candidate_received", self, "candidate_received") + + connect("lobby_joined", self, "lobby_joined") + connect("lobby_sealed", self, "lobby_sealed") + connect("peer_connected", self, "peer_connected") + connect("peer_disconnected", self, "peer_disconnected") + +func start(url, lobby = ""): + stop() + sealed = false + self.lobby = lobby + connect_to_url(url) + +func stop(): + rtc_mp.close() + close() + +func _create_peer(id): + var peer: WebRTCPeerConnection = WebRTCPeerConnection.new() + peer.initialize({ + "iceServers": [ { "urls": ["stun:stun.l.google.com:19302"] } ] + }) + peer.connect("session_description_created", self, "_offer_created", [id]) + peer.connect("ice_candidate_created", self, "_new_ice_candidate", [id]) + rtc_mp.add_peer(peer, id) + if id > rtc_mp.get_unique_id(): + peer.create_offer() + return peer + +func _new_ice_candidate(mid_name, index_name, sdp_name, id): + send_candidate(id, mid_name, index_name, sdp_name) + +func _offer_created(type, data, id): + if not rtc_mp.has_peer(id): + return + print("created", type) + rtc_mp.get_peer(id).connection.set_local_description(type, data) + if type == "offer": send_offer(id, data) + else: send_answer(id, data) + +func connected(id): + print("Connected %d" % id) + rtc_mp.initialize(id, true) + +func lobby_joined(lobby): + self.lobby = lobby + +func lobby_sealed(): + sealed = true + +func disconnected(): + print("Disconnected: %d: %s" % [code, reason]) + if not sealed: + stop() # Unexpected disconnect + +func peer_connected(id): + print("Peer connected %d" % id) + _create_peer(id) + +func peer_disconnected(id): + if rtc_mp.has_peer(id): rtc_mp.remove_peer(id) + +func offer_received(id, offer): + print("Got offer: %d" % id) + if rtc_mp.has_peer(id): + rtc_mp.get_peer(id).connection.set_remote_description("offer", offer) + +func answer_received(id, answer): + print("Got answer: %d" % id) + if rtc_mp.has_peer(id): + rtc_mp.get_peer(id).connection.set_remote_description("answer", answer) + +func candidate_received(id, mid, index, sdp): + if rtc_mp.has_peer(id): + rtc_mp.get_peer(id).connection.add_ice_candidate(mid, index, sdp) diff --git a/project.godot b/project.godot index 980bc49..b42a0a1 100644 --- a/project.godot +++ b/project.godot @@ -11,8 +11,13 @@ config_version=4 [application] config/name="kdt" +run/main_scene="res://main.tscn" config/icon="res://icon.png" +[gdnative] + +singletons=[ "res://webrtc/webrtc.tres" ] + [physics] common/enable_pause_aware_picking=true diff --git a/readme.md b/readme.md index 5ad06d7..aec470c 100644 --- a/readme.md +++ b/readme.md @@ -1,3 +1,7 @@ +# Setup + +- Download the latest WebRTC GDNative plugin from https://github.com/godotengine/webrtc-native/releases and extract it in the project root. + # Signalling Server deno run -A server.ts diff --git a/server.ts b/server.ts index 3884a74..f339351 100644 --- a/server.ts +++ b/server.ts @@ -101,10 +101,14 @@ class Lobby { const assigned = this.getPeerId(peer); const close = assigned === 1; this.peers.forEach((p) => { - // Room host disconnected, must close. - if (close) p.ws.close(4000, STR_HOST_DISCONNECTED); - // Notify peer disconnect. - else p.ws.send(`D: ${assigned}\n`); + try { + // Room host disconnected, must close. + if (close) p.ws.close(4000, STR_HOST_DISCONNECTED); + // Notify peer disconnect. + else p.ws.send(`D: ${assigned}\n`); + } catch (e) { + console.error(`Error when leaving: ${e}`); + } }); this.peers.splice(idx, 1); if (close && this.closeTimer >= 0) { @@ -217,50 +221,57 @@ function parseMsg(peer: Peer, msg: string) { } console.log(`Server running on port ${PORT}`); -const conn = Deno.listen({ port: PORT }); -const httpConn = Deno.serveHttp(await conn.accept()); -const e = await httpConn.nextRequest(); -if (e) { - const { socket, response } = Deno.upgradeWebSocket(e.request); - const id = randomId(); - const peer = new Peer(id, socket); - socket.onopen = (_ev) => { - if (peersCount >= MAX_PEERS) { - socket.close(4000, STR_TOO_MANY_PEERS); - return; +const server = Deno.listen({ port: PORT }); +for await (const conn of server) { + (async () => { + const httpConn = Deno.serveHttp(conn); + for await (const requestEvent of httpConn) { + if (requestEvent) { + const { socket, response } = Deno.upgradeWebSocket( + requestEvent.request, + ); + const id = randomId(); + const peer = new Peer(id, socket); + socket.onopen = (_ev) => { + if (peersCount >= MAX_PEERS) { + socket.close(4000, STR_TOO_MANY_PEERS); + return; + } + peersCount++; + }; + socket.onmessage = (ev) => { + try { + parseMsg(peer, ev.data); + } catch (e) { + const code = e.code || 4000; + console.log(`Error parsing message from ${id}:\n` + ev.data); + socket.close(code, e.message); + } + }; + socket.onclose = (ev) => { + peersCount--; + console.log( + `Connection with peer ${peer.id} closed ` + + `with reason: ${ev.reason}`, + ); + if ( + peer.lobby && + lobbies.has(peer.lobby) && + lobbies.get(peer.lobby).leave(peer) + ) { + lobbies.delete(peer.lobby); + console.log(`Deleted lobby ${peer.lobby}`); + console.log(`Open lobbies: ${lobbies.size}`); + peer.lobby = ""; + } + if (peer.timeout >= 0) { + clearTimeout(peer.timeout); + peer.timeout = -1; + } + }; + socket.onerror = (e) => console.error("WebSocket error:", e); + requestEvent.respondWith(response); + } } - peersCount++; - }; - socket.onmessage = (ev) => { - try { - parseMsg(peer, ev.data); - } catch (e) { - const code = e.code || 4000; - console.log(`Error parsing message from ${id}:\n` + ev.data); - socket.close(code, e.message); - } - }; - socket.onclose = (ev) => { - peersCount--; - console.log( - `Connection with peer ${peer.id} closed ` + - `with reason: ${ev.reason}`, - ); - if ( - peer.lobby && - lobbies.has(peer.lobby) && - lobbies.get(peer.lobby).leave(peer) - ) { - lobbies.delete(peer.lobby); - console.log(`Deleted lobby ${peer.lobby}`); - console.log(`Open lobbies: ${lobbies.size}`); - peer.lobby = ""; - } - if (peer.timeout >= 0) { - clearTimeout(peer.timeout); - peer.timeout = -1; - } - }; - socket.onerror = (e) => console.error("WebSocket error:", e); - e.respondWith(response); + })(); } diff --git a/ws_webrtc_client.gd b/ws_webrtc_client.gd new file mode 100644 index 0000000..3105cf4 --- /dev/null +++ b/ws_webrtc_client.gd @@ -0,0 +1,116 @@ +extends Node + +export var autojoin = true +export var lobby = "" # Will create a new lobby if empty. + +var client: WebSocketClient = WebSocketClient.new() +var code = 1000 +var reason = "Unknown" + +signal lobby_joined(lobby) +signal connected(id) +signal disconnected() +signal peer_connected(id) +signal peer_disconnected(id) +signal offer_received(id, offer) +signal answer_received(id, answer) +signal candidate_received(id, mid, index, sdp) +signal lobby_sealed() + +func _init(): + client.connect("data_received", self, "_parse_msg") + client.connect("connection_established", self, "_connected") + client.connect("connection_closed", self, "_closed") + client.connect("connection_error", self, "_closed") + client.connect("server_close_request", self, "_close_request") + +func connect_to_url(url): + close() + code = 1000 + reason = "Unknown" + client.connect_to_url(url) + +func close(): + client.disconnect_from_host() + +func _closed(was_clean = false): + emit_signal("disconnected") + +func _close_request(code, reason): + self.code = code + self.reason = reason + +func _connected(protocol = ""): + client.get_peer(1).set_write_mode(WebSocketPeer.WRITE_MODE_TEXT) + if autojoin: + join_lobby(lobby) + +func _parse_msg(): + var pkt_str: String = client.get_peer(1).get_packet().get_string_from_utf8() + + var req: PoolStringArray = pkt_str.split("\n", true, 1) + if req.size() != 2: # Invalid request size + return + + var type: String = req[0] + if type.length() < 3: # Invalid type size + return + + if type.begins_with("J: "): + emit_signal("lobby_joined", type.substr(3, type.length() - 3)) + return + elif type.begins_with("S: "): + emit_signal("lobby_sealed") + return + + var src_str: String = type.substr(3, type.length() - 3) + if not src_str.is_valid_integer(): # Source id is not an integer + return + + var src_id: int = int(src_str) + + if type.begins_with("I: "): + emit_signal("connected", src_id) + elif type.begins_with("N: "): + # Client connected + emit_signal("peer_connected", src_id) + elif type.begins_with("D: "): + # Client connected + emit_signal("peer_disconnected", src_id) + elif type.begins_with("O: "): + # Offer received + emit_signal("offer_received", src_id, req[1]) + elif type.begins_with("A: "): + # Answer received + emit_signal("answer_received", src_id, req[1]) + elif type.begins_with("C: "): + # Candidate received + var candidate: PoolStringArray = req[1].split("\n", false) + if candidate.size() != 3: + return + if not candidate[1].is_valid_integer(): + return + emit_signal("candidate_received", src_id, candidate[0], int(candidate[1]), candidate[2]) + +func join_lobby(lobby): + return client.get_peer(1).put_packet(("J: %s\n" % lobby).to_utf8()) + +func seal_lobby(): + return client.get_peer(1).put_packet("S: \n".to_utf8()) + +func send_candidate(id, mid, index, sdp) -> int: + return _send_msg("C", id, "\n%s\n%d\n%s" % [mid, index, sdp]) + +func send_offer(id, offer) -> int: + return _send_msg("O", id, offer) + +func send_answer(id, answer) -> int: + return _send_msg("A", id, answer) + +func _send_msg(type, id, data) -> int: + return client.get_peer(1).put_packet(("%s: %d\n%s" % [type, id, data]).to_utf8()) + +func _process(delta): + var status: int = client.get_connection_status() + if status == WebSocketClient.CONNECTION_CONNECTING or status == WebSocketClient.CONNECTION_CONNECTED: + client.poll()