Work in progress

The content of this page was not yet updated for Godot 4.4 and may be outdated. If you know how to improve this page or you can confirm that it's up to date, feel free to open a pull request.

WebRTС

HTML5, WebSocket, WebRTC

Одна из мощных возможностей Godot — экспорт на платформу HTML5/WebAssembly, что позволяет вашей игре запускаться прямо в браузере при посещении пользователем вашей веб-страницы.

Это отличная возможность как для демо, так и для полноценных игр, но раньше здесь были ограничения. В сфере сетевых взаимодействий браузеры до недавнего времени поддерживали только HTTP-запросы, пока WebSocket, а затем и WebRTC не стали стандартами.

WebSocket

Когда протокол WebSocket был стандартизирован в декабре 2011 года, это позволило браузерам создавать стабильные двусторонние соединения с WebSocket-сервером. Этот протокол является мощным инструментом для отправки push-уведомлений в браузеры и используется для реализации чатов, пошаговых игр и т.д.

Однако соединения WebSocket по-прежнему используют TCP, что обеспечивает надёжность, но не снижает задержек, поэтому они не подходят для приложений реального времени, таких как VoIP и динамичные игры.

WebRTС

По этой причине, начиная с 2010 года, Google начал разработку новой технологии WebRTC, которая позже, в 2017 году, стала кандидатом в рекомендации W3C. WebRTC представляет собой значительно более сложный набор спецификаций, использующий множество вспомогательных технологий (ICE, DTLS, SDP) для обеспечения быстрого, реального времени и безопасного соединения между двумя пирами.

Основная идея — найти самый быстрый маршрут между двумя пирами и установить прямое соединение там, где это возможно (т.е. избежать использования ретранслирующего сервера).

Но за это приходится платить: перед началом связи пиры должны обменяться медиа-информацией в виде строк протокола описания сессии (SDP). Обычно для этого используется специальный сигнальный сервер WebRTC.

../../_images/webrtc_signaling.png

Пиры подключаются к сигнальному серверу (например, WebSocket-серверу) и отправляют свою медиа-информацию. Сервер передаёт эту информацию другим пирам, позволяя им установить желаемое прямое соединение. После выполнения этого шага пиры могут отключиться от сигнального сервера, сохраняя прямое P2P-соединение активным.

WebRTC в контексте Godot

WebRTC реализован в Godot через два основных класса: WebRTCPeerConnection и WebRTCDataChannel, плюс реализацию мультиплеерного API WebRTCMultiplayerPeer. Подробнее см. раздел high-level multiplayer.

Примечание

Эти классы доступны автоматически на платформе HTML5, но требуют внешнего GDExtension-плагина для нативных (не-HTML5) платформ. Инструкции и последние версии смотрите в репозитории плагина webrtc-native и разделе релизов.

Предупреждение

При экспорте под Android обязательно включите разрешение INTERNET в пресете экспорта Android до экспорта проекта или использования одноразвёртывания. Иначе любое сетевое взаимодействие будет заблокировано системой Android.

Простой пример соединения

В этом примере показано, как создать WebRTC-соединение между двумя пирами в одном приложении. Хотя такой подход мало полезен в реальных сценариях, он даст вам чёткое представление о процессе настройки 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.session_description_created.connect(p1.set_local_description)
    # Connect P1 session and ICE created to p2 set remote description and candidates.
    p1.session_description_created.connect(p2.set_remote_description)
    p1.ice_candidate_created.connect(p2.add_ice_candidate)

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

    # Let P1 create the offer
    p1.create_offer()

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

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

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

Вывод в консоли:

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

Пример локального сигналлинга

Этот пример развивает предыдущий: пиры разделены на две разные сцены, а в качестве сигнального сервера используется синглтон (автозагружаемый скрипт).

extends Node
# An example p2p chat client.

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.ice_candidate_created.connect(self._on_ice_candidate)
    peer.session_description_created.connect(self._on_session)

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


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


func _on_session(type, sdp):
    # Send the session to other peer via signaling server.
    Signaling.send_session(String(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(String(get_path()), " received: ", channel.get_packet().get_string_from_utf8())


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

Теперь для локального сигнального сервера:

Примечание

Этот локальный сигнальный сервер должен использоваться как синглтон (автозагружаемый скрипт) для соединения двух пиров в одной сцене.

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

Теперь его можно использовать вот так:

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

    # Wait a second and send message from P1
    await get_tree().create_timer(1).timeout
    p1.send_message("Hi from %s" % String(p1.get_path()))

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

Вывод в консоль будет примерно таким:

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

Удалённый сигналлинг через WebSocket

Более продвинутый пример с использованием WebSocket для сигналлинга между пирами и WebRTCMultiplayerPeer доступен в демо-проектах Godot по пути networking/webrtc_signaling.