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)连接打开状态.

在 Godot 中使用 WebRTC

在Godot中,WebRTC是通过两个主要的类来实现的 WebRTCPeerConnectionWebRTCDataChannel, 加上多人游戏API实现 WebRTCMultiplayer. 更多细节请参见 high-level multiplayer 章节.

备注

这些类在HTML5中自动可用, 但 需要在本地(非HTML5)平台上使用外部GDNative插件 . 查看 webrtc-native 插件库 , 以获取说明和最新的 发布 .

警告

当导出到 Android 时,在导出项目或使用一键部署之前,确保在 Android 导出预设中启用 INTERNET 权限。否则,任何形式的网络通信都会被 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.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())

这将打印:

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

本地信号示例

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

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

现在是本地信号服务器:

备注

这个本地信号服务器应该是作为一个 singleton 来连接同一场景中的两个对等体.

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

然后, 您可以这样使用它:

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

将打印出类似这样的内容:

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

使用 WebSocket 进行远程信号传输

一个更高级的演示,使用 WebSocket 作为信号对等体和 WebRTCMultiplayergodot 演示项目 networking/webrtc_signaling 下提供。