WebRTC

HTML5, WebSocket, WebRTC

Eine der großartigen Funktionen von Godot ist die Möglichkeit, auf die HTML5/WebAssembly-Plattform zu exportieren, sodass Ihr Spiel direkt im Browser ausgeführt werden kann, wenn ein Benutzer Ihre Webseite besucht.

Dies ist eine großartige Gelegenheit sowohl für Demos als auch für vollständige Spiele, war jedoch mit einigen Einschränkungen verbunden. Im Bereich Netzwerk unterstützten Browser bis vor kurzem nur HTTPRequests, bis erst WebSocket und dann WebRTC als Standards vorgeschlagen wurden.

WebSocket

Als das WebSocket-Protokoll im Dezember 2011 standardisiert wurde, konnten Browser stabile und bidirektionale Verbindungen zu einem WebSocket-Server herstellen. Das Protokoll ist recht einfach, aber ein sehr leistungsfähiges Tool zum Senden von Push-Benachrichtigungen an Browser. Es wurde zum Implementieren von Chats, rundenbasierten Spielen usw. verwendet.

WebSockets verwenden jedoch weiterhin eine TCP-Verbindung, die sich positiv auf die Zuverlässigkeit, aber nicht auf die Latenz auswirkt und daher nicht für Echtzeitanwendungen wie VoIP und schnelle Spiele geeignet ist.

WebRTC

Aus diesem Grund begann Google seit 2010 mit der Arbeit an einer neuen Technologie namens WebRTC, die später im Jahr 2017 zu einer Empfehlung für W3C-Kandidaten wurde. WebRTC ist ein viel komplexerer Satz von Spezifikationen und stützt sich auf viele andere Technologien hinter den Kulissen (ICE, DTLS, SDP), um eine schnelle, Echtzeit- und sichere Kommunikation zwischen zwei Peers zu ermöglichen.

Die Idee ist, den schnellsten Weg zwischen den beiden Peers zu finden und wann immer möglich eine direkte Kommunikation herzustellen (d.h. einen Weiterleitungsserver/Relay zu vermeiden).

Dies hat jedoch einen Preis: Einige Medieninformationen müssen zwischen den beiden Peers ausgetauscht werden, bevor die Kommunikation beginnen kann (in Form von Session Description Protocol - SDP-Zeichenfolgen). Dies erfolgt üblicherweise in Form eines sogenannten WebRTC-Signalisierungsservers.

../../_images/webrtc_signaling.png

Peers stellen eine Verbindung zu einem Signalisierungsserver (z.B. einem WebSocket-Server) her und senden ihre Medieninformationen. Der Server leitet diese Informationen dann an andere Peers weiter, sodass diese die gewünschte direkte Kommunikation herstellen können. Sobald dieser Schritt abgeschlossen ist, können Peers die Verbindung zum Signalisierungsserver trennen und die direkte Peer-to-Peer-Verbindung (P2P) offen halten.

Nutzung von WebRTC in Godot

WebRTC wird in Godot über zwei Hauptklassen implementiert WebRTCPeerConnection und WebRTCDataChannel sowie die Multiplayer-API-Implementierung WebRTCMultiplayer.

Bemerkung

Diese Klassen sind automatisch in HTML5 verfügbar, erfordern jedoch ein externes GDNative-Plugin auf nativen (Nicht-HTML5-) Plattformen. Anweisungen und die neueste Version finden Sie im webrtc-native plugin repository.

minimales Verbindungs Beispiel

Dieses Beispiel zeigt Ihnen, wie Sie eine WebRTC-Verbindung zwischen zwei Peers in derselben Anwendung erstellen. Dies ist im wirklichen Leben nicht sehr nützlich, gibt Ihnen jedoch einen guten Überblick über den Aufbau einer WebRTC-Verbindung.

extends Node

# Create the two peers
var p1 = WebRTCPeerConnection.new()
var p2 = WebRTCPeerConnection.new()
# And a negotiated channel for each each peer
var ch1 = p1.create_data_channel("chat", {"id": 1, "negotiated": true})
var ch2 = p2.create_data_channel("chat", {"id": 1, "negotiated": true})

func _ready():
    # Connect P1 session created to itself to set local description
    p1.connect("session_description_created", p1, "set_local_description")
    # Connect P1 session and ICE created to p2 set remote description and candidates
    p1.connect("session_description_created", p2, "set_remote_description")
    p1.connect("ice_candidate_created", p2, "add_ice_candidate")

    # Same for P2
    p2.connect("session_description_created", p2, "set_local_description")
    p2.connect("session_description_created", p1, "set_remote_description")
    p2.connect("ice_candidate_created", p1, "add_ice_candidate")

    # Let P1 create the offer
    p1.create_offer()

    # Wait a second and send message from P1
    yield(get_tree().create_timer(1), "timeout")
    ch1.put_packet("Hi from P1".to_utf8())

    # Wait a second and send message from P2
    yield(get_tree().create_timer(1), "timeout")
    ch2.put_packet("Hi from P2".to_utf8())

func _process(_delta):
    # Poll connections
    p1.poll()
    p2.poll()

    # Check for messages
    if ch1.get_ready_state() == ch1.STATE_OPEN and ch1.get_available_packet_count() > 0:
        print("P1 received: ", ch1.get_packet().get_string_from_utf8())
    if ch2.get_ready_state() == ch2.STATE_OPEN and ch2.get_available_packet_count() > 0:
        print("P2 received: ", ch2.get_packet().get_string_from_utf8())

Dies zeigt:

P1 received: Hi from P1
P2 received: Hi from P2

lokales Signalisierungs-Beispiel

Dieses Beispiel erweitert das vorherige Beispiel, trennt die Peers in zwei verschiedenen Szenen und verwendet ein Singleton als Signalisierungsserver.

# An example P2P chat client (chat.gd)
extends Node

var peer = WebRTCPeerConnection.new()

# Create negotiated data channel
var channel = peer.create_data_channel("chat", {"negotiated": true, "id": 1})

func _ready():
    # Connect all functions
    peer.connect("ice_candidate_created", self, "_on_ice_candidate")
    peer.connect("session_description_created", self, "_on_session")

    # Register to the local signaling server (see below for the implementation)
    Signaling.register(get_path())

func _on_ice_candidate(mid, index, sdp):
    # Send the ICE candidate to the other peer via signaling server
    Signaling.send_candidate(get_path(), mid, index, sdp)

func _on_session(type, sdp):
    # Send the session to other peer via signaling server
    Signaling.send_session(get_path(), type, sdp)
    # Set generated description as local
    peer.set_local_description(type, sdp)

func _process(delta):
    # Always poll the connection frequently
    peer.poll()
    if channel.get_ready_state() == WebRTCDataChannel.STATE_OPEN:
        while channel.get_available_packet_count() > 0:
            print(get_path(), " received: ", channel.get_packet().get_string_from_utf8())

func send_message(message):
    channel.put_packet(message.to_utf8())

Kommen wir jetzt zum lokalen Signalisierungsserver:

Bemerkung

Dieser lokale Signalisierungsserver soll als Singleton verwendet werden um zwei Peers in derselben Szene zu verbinden.

# A local signaling server. Add this to autoloads with name "Signaling" (/root/Signaling)
extends Node

# We will store the two peers here
var peers = []

func register(path):
    assert(peers.size() < 2)
    peers.append(path)
    # If it's the second one, create an offer
    if peers.size() == 2:
        get_node(peers[0]).peer.create_offer()

func _find_other(path):
    # Find the other registered peer.
    for p in peers:
        if p != path:
            return p
    return ""

func send_session(path, type, sdp):
    var other = _find_other(path)
    assert(other != "")
    get_node(other).peer.set_remote_description(type, sdp)

func send_candidate(path, mid, index, sdp):
    var other = _find_other(path)
    assert(other != "")
    get_node(other).peer.add_ice_candidate(mid, index, sdp)

Nun kann es wie folgt genutzt werden:

# Main scene (main.gd)
extends Node

const Chat = preload("res://chat.gd")

func _ready():
    var p1 = Chat.new()
    var p2 = Chat.new()
    add_child(p1)
    add_child(p2)
    yield(get_tree().create_timer(1), "timeout")
    p1.send_message("Hi from %s" % p1.get_path())

    # Wait a second and send message from P2
    yield(get_tree().create_timer(1), "timeout")
    p2.send_message("Hi from %s" % p2.get_path())

Dies wird etwas ähnliches wie dies hier ausgeben:

/root/main/@@3 received: Hi from /root/main/@@2
/root/main/@@2 received: Hi from /root/main/@@3

Remote-Signalisierung mit WebSocket

Ein fortgeschrittenes Beispiel mit WebSocket zum Signalisieren von Peers und WebRTCMultiplayer ist in den godot demo projects unter`networking/webrtc_signaling` verfügbar.