Work in progress

The content of this page was not yet updated for Godot 4.2 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.

WebRTC

HTML5、WebSocket、WebRTC

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

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

WebSocket

When the WebSocket protocol was standardized in December 2011, it allowed browsers to create stable and bidirectional connections to a WebSocket server. The protocol is a very powerful tool to send push notifications to browsers, and has been used to implement chats, turn-based games, etc.

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

WebRTC

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

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

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

../../_images/webrtc_signaling.png

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

在 Godot 中使用 WebRTC

WebRTC is implemented in Godot via two main classes WebRTCPeerConnection and WebRTCDataChannel, plus the multiplayer API implementation WebRTCMultiplayerPeer. See section on high-level multiplayer for more details.

备注

These classes are available automatically in HTML5, but require an external GDExtension plugin on native (non-HTML5) platforms. Check out the webrtc-native plugin repository for instructions and to get the latest release.

警告

当导出到 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.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

本地信号示例

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

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

现在是本地信号服务器:

备注

这个本地信号服务器应该是作为一个 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 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 进行远程信号传输

A more advanced demo using WebSocket for signaling peers and WebRTCMultiplayerPeer is available in the godot demo projects under networking/webrtc_signaling.