Initial commit

This commit is contained in:
Daniel Flanagan 2021-11-15 15:02:55 -06:00
parent 308f09916b
commit b915552a64
Signed by: lytedev
GPG key ID: 5B2020A0F9921EF4
11 changed files with 557 additions and 49 deletions

1
.gitignore vendored
View file

@ -4,3 +4,4 @@ export_presets.cfg
*.translation
.mono
data_*/
/webrtc/

64
client_ui.gd Normal file
View file

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

107
client_ui.tscn Normal file
View file

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

View file

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

14
main.gd Normal file
View file

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

98
main.tscn Normal file
View file

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

86
multiplayer_client.gd Normal file
View file

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

View file

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

View file

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

109
server.ts
View file

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

116
ws_webrtc_client.gd Normal file
View file

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