WebRTC

HTML5,WebSocket,WebRTC

Godot的一大特点是它能够导出到HTML5/WebAssembly平台,当用户访问您的网页时,您的游戏可以直接在浏览器中运行。

这对于演示和完整的游戏来说都是一个很好的机会,但过去有一些限制,在网络领域,浏览器过去只支持HTTPRequests,直到最近,首先是WebSocket,然后是WebRTC被提出为标准。

WebSocket

当WebSocket协议在2011年12月被标准化后,它允许浏览器与WebSocket服务器建立稳定的双向连接。该协议相当简单,但却是一个非常强大的向浏览器发送推送通知的工具,并已被用于实现聊天、回合制游戏等。

不过,WebSockets仍然使用TCP连接,这对可靠性有好处,但对减少延迟没有好处,所以不适合实时应用,比如VoIP和快节奏的游戏。

WebRTC

为此,从2010年开始,谷歌开始研究一项名为WebRTC的新技术,后来在2017年,这项技术成为W3C候选推荐。WebRTC是一套复杂得集合规范,并且在后台依靠许多其他技术(ICE、DTLS、SDP)来提供两个对等体之间快速、实时、安全的通信。

其想法是找到两个对等体之间最快的路线,并尽可能建立直接通信(尽量避开中继服务器)。

然而,这是有代价的,那就是在通信开始之前,两个对等体之间必须交换一些媒介信息(以会话描述协议--SDP字符串的形式)。这通常采取所谓的WebRTC信号服务器的形式。

../../_images/webrtc_signaling.png

对等体连接到信号服务器(例如 WebSocket 服务器)并发送其媒介信息。然后,服务器将此信息转发到其他对等体,允许它们建立所需的直接通信。这一步完成后,对等体可以断开与信号服务器的连接,并保持直接的点对点(P2P)连接打开状态。

Using WebRTC in Godot

在Godot中,WebRTC是通过两个主要的类来实现的 WebRTCPeerConnection,加上多人游戏API实现 WebRTCMultiplayer。更多细节请参见 :ref:`high-level multiplayer <doc_high_level_multiplayer>`章节。

注解

这些类在HTML5中自动可用,但**需要在本地(非HTML5)平台上使用外部GDNative插件**。查看`webrtc-native插件库<https://github.com/godotengine/webrtc-native>`__,以获取说明和最新的`发布<https://github.com/godotengine/webrtc-native/releases>`__。

Minimal connection example

这个例子将向您展示如何在同一应用程序中的两个对等体之间创建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.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())

This will print:

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

Local signaling example

这个例子在上一个例子的基础上进行了扩展,将对等体分离在两个不同的场景中,并使用:ref:`singleton <doc_singletons_autoload>`作为信号服务器。

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

现在是本地信号服务器:

注解

这个本地信号服务器应该是作为一个:ref:`singleton <doc_singletons_autoload>`来连接同一场景中的两个对等体。

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

Then you can use it like this:

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

This will print something similar to this:

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

Remote signaling with WebSocket

一个更高级的演示,使用WebSocket作为信号对等体和:ref:`WebRTCMultiplayer <class_WebRTCMultiplayer>`在`godot演示项目<https://github.com/godotengine/godot-demo-projects>`_`networking/webrtc_signaling`下提供。