WebRTC

HTML5, WebSocket, WebRTC

Una de las grandes características de Godot es su capacidad de exportar a la plataforma HTML5/WebAssembly, lo que permite que el juego se ejecute directamente en el navegador cuando un usuario visita su página web.

Esta es una gran oportunidad tanto para las demostraciones como para los juegos completos, pero solía venir con algunas limitaciones. En el área de redes, los navegadores solían soportar sólo HTTPRequests hasta hace poco, cuando primero se propusieron como estándares WebSocket y luego WebRTC.

Web Socket

Cuando el protocolo WebSocket fue estandarizado en diciembre de 2011, permitió a los navegadores crear conexiones estables y bidireccionales con un servidor WebSocket. El protocolo es bastante simple, pero es una herramienta muy poderosa para enviar notificaciones push a los navegadores, y se ha utilizado para implementar chats, juegos por turnos, etc.

Los WebSockets, sin embargo, siguen usando una conexión TCP, lo que es bueno para la fiabilidad pero no para la latencia, por lo que no es bueno para aplicaciones en tiempo real como VoIP y juegos de ritmo rápido.

WebRTC

Por esta razón, desde 2010, Google comenzó a trabajar en una nueva tecnología llamada WebRTC, que más tarde, en 2017, se convirtió en una recomendación de candidato del W3C. WebRTC es un conjunto de especificaciones mucho más complejo, y se basa en muchas otras tecnologías entre bastidores (ICE, DTLS, SDP) para proporcionar una comunicación rápida, en tiempo real y segura entre dos pares.

La idea es encontrar la ruta más rápida entre los dos pares y establecer siempre que sea posible una comunicación directa (es decir, tratar de evitar un servidor de retransmisión).

Sin embargo, esto tiene un precio, que es que cierta información de los medios de comunicación debe ser intercambiada entre los dos pares antes de que la comunicación pueda comenzar (en forma de Protocolo de Descripción de Sesión - cadenas SDP). Esto normalmente toma la forma de un llamado Servidor de Señalización WebRTC.

../../_images/webrtc_signaling.png

Los pares se conectan a un servidor de señales (por ejemplo, un servidor WebSocket) y envían su información de medios. El servidor luego transmite esta información a otros pares, permitiéndoles establecer la comunicación directa deseada. Una vez realizado este paso, los pares pueden desconectarse del servidor de señalización y mantener abierta la conexión directa Peer-to-Peer (P2P).

Usando WebRTC en Godot

WebRTC está implementado en Godot a través de dos clases principales WebRTCPeerConnection y WebRTCDataChannel, además de la implementación de la API multijugador WebRTCMultiplayer. Ver la sección sobre highh-level multiplayer para más detalles.

Nota

Estas clases están disponibles automáticamente en HTML5, pero requieren un plugin GDNative externo en plataformas nativas (no HTML5). Revisa el repositorio de plugins nativos de la web <https://github.com/godotengine/webrtc-native>`__ para instrucciones y para obtener el último lanzamiento.

Advertencia

Cuando exportes a Android, asegúrate de habilitar el permiso INTERNET en la configuración de exportación de Android antes de exportar el proyecto o utilizar el despliegue de un solo clic. De lo contrario, la comunicación de red de cualquier tipo será bloqueada por Android.

Ejemplo mínimo de conexión

Este ejemplo le mostrará cómo crear una conexión WebRTC entre dos pares en la misma aplicación. Esto no es muy útil en la vida real, pero te dará una buena visión general de cómo se establece una conexión WebRTC.

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

Esto imprimirá:

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

Ejemplo de señales locales

Este ejemplo se expande sobre el anterior, separando los pares en dos escenas diferentes, y usando un singleton como servidor de señales.

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

Y ahora para el servidor de señales locales:

Nota

Este servidor local de señales se supone que debe ser usado como un singleton para conectar dos pares en la misma escena.

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

Luego puedes usarlas así:

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

Esto imprimirá algo similar a esto:

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

Señalización remota con WebSocket

Una demostración más avanzada usando WebSocket para la señalización de pares y WebRTCMultiplayer está disponible en los proyectos de demostración de Godot bajo networking/webrtc_signaling.