WebRTC

HTML5, WebSocket, WebRTC

L'une des grandes caractéristiques de Godot est sa capacité d'exportation vers la plateforme HTML5/WebAssembly, permettant à votre jeu de s'exécuter directement dans le navigateur lorsqu'un utilisateur visite votre page web.

Il s'agit d'une excellente opportunité pour les démos et les jeux complets, mais elle était auparavant assortie de certaines limitations. Dans le domaine de la mise en réseau, les navigateurs ne prenaient en charge que les requêtes HTTPR jusqu'à récemment, lorsque WebSocket puis WebRTC ont été proposés comme normes.

WebSocket

Lorsque le protocole WebSocket a été normalisé en décembre 2011, il a permis aux navigateurs de créer des connexions stables et bidirectionnelles à un serveur WebSocket. Le protocole est assez simple, mais c'est un outil très puissant pour envoyer des notifications "push" aux navigateurs, et il a été utilisé pour mettre en place des chats, des jeux au tour par tour, etc.

Les WebSockets, cependant, utilisent toujours une connexion TCP, ce qui est bon pour la fiabilité mais pas pour la latence, donc pas bon pour les applications en temps réel comme la VoIP et les jeux rapides.

WebRTC

C'est pourquoi, depuis 2010, Google a commencé à travailler sur une nouvelle technologie appelée WebRTC, qui plus tard, en 2017, est devenue une recommandation candidate du W3C. Le WebRTC est un ensemble de spécifications beaucoup plus complexe, et s'appuie sur de nombreuses autres technologies en coulisse (ICE, DTLS, SDP) pour assurer une communication rapide, en temps réel et sécurisée entre deux pairs.

L'idée est de trouver le chemin le plus rapide entre les deux pairs et d'établir, dans la mesure du possible, une communication directe (c'est-à-dire essayer d'éviter un serveur relais).

Cependant, cela a un prix, c'est-à-dire que certaines informations sur les médias doivent être échangées entre les deux pairs avant que la communication puisse commencer (sous la forme du Session Description Protocol - SDP chaînes). Cela prend généralement la forme d'un Signaling Server WebRTC.

../../_images/webrtc_signaling.png

Les pairs se connectent à un serveur de signalisation (par exemple un serveur WebSocket) et envoient leurs informations média. Le serveur relaie ensuite ces informations à d'autres pairs, leur permettant d'établir la communication directe souhaitée. Une fois cette étape franchie, les pairs peuvent se déconnecter du serveur de signalisation et garder la connexion directe de poste à poste (P2P) ouverte.

Utilisation de WebRTC dans Godot

WebRTC est implémenté dans Godot via deux classes principales WebRTCPeerConnection et WebRTCDataChannel, plus l'implémentation de l'API multijoueur WebRTCMultiplayer. Voir la section high-level multiplayer pour plus de détails.

Note

Ces classes sont disponibles automatiquement en HTML5, mais nécessitent un plugin GDNative externe sur les plateformes natives (non-HTML5). Consultez le dépôt de plugins webrtc-native plugin repository pour obtenir des instructions et la dernière version.

Avertissement

Lors de l'exportation vers Android, assurez-vous d'activer la permission INTERNET dans le préréglage d'exportation Android avant d'exporter le projet ou d'utiliser le déploiement en un clic. Sinon, toute communication réseau sera bloquée par Android.

Exemple minimal de connexion

Cet exemple vous montrera comment créer une connexion WebRTC entre deux pairs dans la même application. Ce n'est pas très utile dans la vie réelle, mais cela vous donnera un bon aperçu de la manière dont une connexion WebRTC est établie.

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

Ceci s'affichera :

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

Exemple de signalisation locale

Cet exemple développe le précédent, en séparant les pairs dans deux scènes différentes, et en utilisant un singleton comme serveur de signalisation.

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

Et maintenant, le serveur de signalisation local :

Note

Ce serveur de signalisation local est censé être utilisé comme un singleton pour connecter deux pairs dans la même scène.

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

Alors vous pouvez l'utiliser comme ça :

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

Cela va imprimer quelque chose de similaire à ceci :

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

Signalisation à distance avec WebSocket

Une démo plus avancée utilisant WebSocket pour la signalisation des pairs et WebRTCMultiplayer est disponible dans les projets godot demo projects sous networing/webrtc_signaling.