WebRTC

HTML5、WebSocket、WebRTC

Godotの優れた機能の1つは、HTML5/WebAssemblyプラットフォームにエクスポートできることです。これにより、ユーザーがWebページにアクセスしたときに、ブラウザでゲームを直接実行できます。

これはデモと完全なゲームの両方にとって素晴らしい機会ですが、以前はいくつかの制限がありました。ネットワークの分野では、ブラウザは最近まで、最初にWebSocket、次にWebRTCが標準として提案されるまではHTTPRequestsのみをサポートしていました。

WebSocket

2011年12月にWebSocketプロトコルが標準化されたとき、ブラウザはWebSocketサーバーへの安定した双方向の接続を作成できました。プロトコルは非常にシンプルですが、ブラウザにプッシュ通知を送信するための非常に強力なツールであり、チャット、ターンベースのゲームなどの実装に使用されています。

ただし、WebSocketは依然としてTCP接続を使用します。TCP接続は信頼性には優れていますが、低遅延には適していません。したがって、VoIPやテンポの速いゲームなどのリアルタイムアプリケーションには適していません。

WebRTC

このため、2010年以降、GoogleはWebRTCと呼ばれる新しいテクノロジーの開発に着手しました。WebRTCは、2017年にW3Cの推奨候補になりました。 WebRTCは、はるかに複雑な仕様のセットであり、2つのピア間で高速でリアルタイムの安全な通信を提供するために、舞台裏にある他の多くのテクノロジー(ICE、DTLS、SDP)に依存しています。

このアイデアは、2つのピア間の最速のルートを見つけて、可能な限り直接通信を確立することです(つまり、中継サーバーを回避しようとします)。

ただし、これには代償が伴います。つまり、通信を開始する前に、2つのピア間で一部のメディア情報を交換する必要があります(セッション記述プロトコル - SDP文字列の形式)。これは通常、いわゆるWebRTC Signaling Serverの形式を取ります。

../../_images/webrtc_signaling.png

ピアはシグナリングサーバー(WebSocketサーバーなど)に接続し、メディア情報を送信します。サーバーはこの情報を他のピアに中継し、ピアが目的の直接通信を確立できるようにします。このステップが完了すると、ピアはシグナリングサーバーから切断し、直接ピアツーピア(P2P)接続を開いたままにすることができます。

GodotでWebRTCを使用する

WebRTCは、2つのメインクラス WebRTCPeerConnection および WebRTCDataChannel に加えて、マルチプレイヤーAPI実装 WebRTCMultiplayer を介してGodotに実装されます。詳細については、high-level multiplayer のセクションを参照してください。

注釈

これらのクラスはHTML5で自動的に使用できますが、ネイティブ(非HTML5) プラットフォームでは外部GDNativeプラグインが必要です。手順については、webrtc-native plugin repository を確認し、最新の release を取得してください。

警告

When exporting to Android, make sure to enable the INTERNET permission in the Android export preset before exporting the project or using one-click deploy. Otherwise, network communication of any kind will be blocked by Android.

最小限の接続例

この例では、同じアプリケーション内の2つのピア間に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

ローカルシグナリングの例

この例は前の例を拡張し、2つの異なるシーンでピアを分離し、シグナリングサーバーとして 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())

そして、ローカルシグナリングサーバーの場合:

注釈

このローカルシグナリングサーバーは、同じシーン内の2つのピアを接続するための 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を使用し、WebRTCMultiplayer を使用したより高度なデモは、 networking/webrtc_signaling 内の godot demo projects にあります。