高レベルのマルチプレイヤー

高レベルAPIと低レベルAPI

以下では、Godotの高レベルネットワークと低レベルネットワークの違いといくつかの基本事項について説明します。基本をとばして最初のノードにネットワークを追加する場合は、以下の Initializing the network までスキップしてください。ただし、後で残りも読んでください!

Godotは、UDP、TCP、およびSSLやHTTPなどの一部の高レベルプロトコルを介した標準の低レベルネットワークを常にサポートしてきました。これらのプロトコルは柔軟性があり、ほとんどすべてに使用できます。ただし、ゲームの状態を手動で同期するためにそれらを使用すると、多大な作業が必要になる場合があります。バックエンドでカスタムサーバー実装を使用する場合など、その作業を回避できない場合や、そうするほうが価値がある場合があります。しかし、ほとんどの場合、使いやすさを高めるために低レベルネットワークのきめ細かな制御の一部を犠牲にしてでも、Godotの高レベルネットワークAPIを検討する価値があります。

これは、低レベルプロトコルの固有の制限によるものです:

  • TCPは、パケットが常に確実に順序どおりに到着することを保証しますが、一般に、エラー修正のために待ち時間が長くなります。また、手順に「接続(connection)」を必要とし、マルチプレイヤーゲームのようなアプリケーションには向かない目的に合わせて最適化されているので、非常に複雑なプロトコルでもあります。パケットはより大きなバッチで送信されるようにバッファリングされ、パケットごとのオーバーヘッドが少なくなり、待ち時間が長くなります。これはHTTPのようなものには便利ですが、一般的にはゲームには役立ちません。この一部は、(例えば、TCP接続の「Nagleのアルゴリズム」を無効にすることによって)設定および無効にすることができます。
  • UDPはより単純なプロトコルであり、パケットのみを送信します(「接続」の概念はありません)。エラー修正を行わないので処理がかなり速くなりますが(低遅延)、パケットは途中で失われたり、間違った順序で受信されたりする可能性があります。それに加えて、UDPのMTU(最大パケットサイズ)は一般的に小さい(数百バイトのみ)ため、大きなパケットを送信することは、パケットを分割し、再編成し、一部が失敗した場合に再試行することを意味します。

一般に、TCPは信頼性が高く、順序があり、低速であると考えることができます。それに対し、UDPは信頼性がなく、順序がなく、高速であるといえます。パフォーマンスが大きく異なるため、多くの場合、ゲームに必要なTCPの部分(オプションの信頼性とパケット順序)を再構築し、不要な部分(輻輳/トラフィック制御機能、Nagleのアルゴリズムなど)を回避することは理にかなっています。このため、ほとんどのゲームエンジンにはこのような実装が付属しており、Godotも例外ではありません。

要約すると、低レベルのネットワークAPIを使用して最大限の制御を行い、すべてを素のネットワークプロトコルの上に実装するか、または `SceneTree <class_SceneTree>`に基づいた高レベルAPIを使用するかです。これは、一般的に最適化された方法で、シーンの後ろで重い処理を引き受けます。

注釈

Godotがサポートするプラットフォームのほとんどは、言及された高レベルおよび低レベルのネットワーク機能のすべてまたはほとんどを提供します。ただし、ネットワークは常にハードウェアとオペレーティングシステムに大きく依存しているため、一部のターゲットプラットフォームでは一部の機能が変更されたり利用できない場合があります。最も顕著ものとしてHTML5プラットフォームは、現在、WebSocketサポートのみを提供しており、TCPやUDPのような低レベルのプロトコルへの未加工のアクセスだけでなく、より高いレベルの機能の一部も欠いています。

注釈

More about TCP/IP, UDP, and networking: https://web.archive.org/web/20190406162102/https://gafferongames.com/post/udp_vs_tcp/

Gaffer On Games has a lot of useful articles about networking in Games (here), including the comprehensive introduction to networking models in games.

Godotの組み込みネットワークではなく、選択した低レベルネットワークライブラリを使用する場合は、次の例を参照してください: https://github.com/PerduGames/gdnet3

警告

ゲームにネットワークを追加するには、ある程度の責任が伴います。間違って実行すると、アプリケーションが脆弱になり、チートや悪用につながる可能性があります。攻撃者がアプリケーションを実行しているマシンを危険にさらし、サーバーを使用してスパムを送信したり、他のユーザーを攻撃したり、ユーザーがゲームをプレイしている場合にユーザーデータを盗んだりすることもできます。

これは、ネットワーキングが関係する場合に常に当てはまり、Godotとは関係ありません。もちろん実験はできますが、ネットワークアプリケーションをリリースするときは、考えられるセキュリティ上の問題に常に注意してください。

中レベルの抽象化

ネットワークを介してゲームを同期する方法に入る前に、同期のための基本のネットワークAPIがどのように機能するかを理解しておくと役立ちます。

Godot uses a mid-level object NetworkedMultiplayerPeer. This object is not meant to be created directly, but is designed so that several implementations can provide it.

このオブジェクトは PacketPeer から拡張されるため、データのシリアル化、送信、受信に役立つすべてのメソッドを継承します。さらに、ピア、転送モードなどを設定するメソッドを追加します。また、ピアが接続または切断したときに通知する信号も含まれています。

This class interface can abstract most types of network layers, topologies and libraries. By default, Godot provides an implementation based on ENet (NetworkedMultiplayerEnet), one based on WebRTC (WebRTCMultiplayer), and one based on WebSocket (WebSocketMultiplayerPeer), but this could be used to implement mobile APIs (for adhoc WiFi, Bluetooth) or custom device/console-specific networking APIs.

ほとんどの場合、Godotはさらに高度なネットワーク機能を提供するため、このオブジェクトを直接使用することはお勧めしません。 それでも、ゲームに低レベルAPIの特定のニーズがある場合に利用できます。

ネットワークの初期化

Godotでネットワークを制御するオブジェクトは、ツリー関連のすべてを制御するオブジェクトと同じです: SceneTree

高レベルのネットワークを初期化するには、SceneTreeにNetworkedMultiplayerPeerオブジェクトを提供する必要があります。

そのオブジェクトを作成するには、最初にサーバーまたはクライアントとして初期化する必要があります。

指定された最大ピア数を使用して、指定されたポートでリッスンするサーバーとして初期化します:

var peer = NetworkedMultiplayerENet.new()
peer.create_server(SERVER_PORT, MAX_PLAYERS)
get_tree().set_network_peer(peer)

クライアントとして初期化して、特定のIPとポートに接続します:

var peer = NetworkedMultiplayerENet.new()
peer.create_client(SERVER_IP, SERVER_PORT)
get_tree().set_network_peer(peer)

以前に設定したネットワーク ピアを取得します:

get_tree().get_network_peer()

ツリーがサーバーまたはクライアントとして初期化されているかどうかを確認します:

get_tree().is_network_server()

ネットワーク機能の終了:

get_tree().set_network_peer(null)

(ゲームによっては、接続を閉じたりタイムアウトしたりするのではなく、他のピアに自分が離れることを知らせるために、最初にメッセージを送信することは理にかなっています。)

接続の管理

Some games accept connections at any time, others during the lobby phase. Godot can be requested to no longer accept connections at any point (see set_refuse_new_network_connections(bool) and related methods on SceneTree). To manage who connects, Godot provides the following signals in SceneTree:

サーバー及びクライアント:

  • network_peer_connected(int id)
  • network_peer_disconnected(int id)

上記のシグナルは、新しいピアが接続または切断したときに、サーバーに接続されているすべてのピア(サーバー上を含む)で呼び出されます。 クライアントは1より大きい一意のIDで接続しますが、ネットワークピアID 1は常にサーバーです。 1未満は無効として処理されます。ローカルシステムのIDは、SceneTree.get_network_unique_id() から取得できます。これらのIDは、主にロビー管理に役立ち、接続されたピア、つまりプレイヤーを識別するため、通常は保存する必要があります。 IDを使用して、特定のピアにのみメッセージを送信することもできます。

クライアント:

  • connected_to_server
  • connection_failed
  • server_disconnected

繰り返しますが、これらの機能はすべて、主にロビー管理やその場でのプレイヤーの追加/削除に役立ちます。 これらのタスクの場合、サーバーは明らかにサーバーとして機能する必要があり、既に接続されている他のプレイヤーに、新しく接続されたプレイヤーに関する情報(名前、統計など)を送信するなどのタスクを手動で実行する必要があります。

ロビーは任意の方法で実装できますが、最も一般的な方法は、すべてのピアのシーンで同じ名前のノードを使用することです。 一般的に、自動ロードされたノード/シングルトンはこれに最適で、常にアクセスできます。例 "/root/lobby"。

RPC

ピア間で通信するための最も簡単な方法は、RPC(リモートプロシージャコール)を使用することです。これは一連の関数として Node で実装されます:

  • rpc("function_name", <optional_args>)
  • rpc_id(<peer_id>,"function_name", <optional_args>)
  • rpc_unreliable("function_name", <optional_args>)
  • rpc_unreliable_id(<peer_id>, "function_name", <optional_args>)

メンバー変数の同期も可能です:

  • rset("variable", value)
  • rset_id(<peer_id>, "variable", value)
  • rset_unreliable("variable", value)
  • rset_unreliable_id(<peer_id>, "variable", value)

関数は、次の2つの方法で呼び出すことができます:

  • 信頼性: 関数呼び出しは何があっても到着しますが、失敗した場合は再送信されるため、時間がかかる場合があります。
  • 信頼性が低い: 関数呼び出しが到着しない場合、再送信されません。しかし、それが到着した場合、すぐにそれを実行します。

ほとんどの場合、信頼性が求められます。 Unreliableは、オブジェクトの位置を同期するときに主に役立ちます(同期は絶えず発生する必要があり、パケットが失われた場合でも、その後に新しいパケットが到着し、その間にオブジェクトがさらに移動したために、失われたパケットの情報は古くなっている可能性があるため、無くなってもさほど問題はありません。ほぼ、これは再送と同じ結果です)。

There is also the get_rpc_sender_id function in SceneTree, which can be used to check which peer (or peer ID) sent an RPC.

ロビーに戻る

ロビーに戻りましょう。サーバーに接続する各プレイヤーが、そのことを全員に伝えることを想像してみてください。

# Typical lobby implementation; imagine this being in /root/lobby.

extends Node

# Connect all functions

func _ready():
    get_tree().connect("network_peer_connected", self, "_player_connected")
    get_tree().connect("network_peer_disconnected", self, "_player_disconnected")
    get_tree().connect("connected_to_server", self, "_connected_ok")
    get_tree().connect("connection_failed", self, "_connected_fail")
    get_tree().connect("server_disconnected", self, "_server_disconnected")

# Player info, associate ID to data
var player_info = {}
# Info we send to other players
var my_info = { name = "Johnson Magenta", favorite_color = Color8(255, 0, 255) }

func _player_connected(id):
    # Called on both clients and server when a peer connects. Send my info to it.
    rpc_id(id, "register_player", my_info)

func _player_disconnected(id):
    player_info.erase(id) # Erase player from info.

func _connected_ok():
    pass # Only called on clients, not server. Will go unused; not useful here.

func _server_disconnected():
    pass # Server kicked us; show error and abort.

func _connected_fail():
    pass # Could not even connect to server; abort.

remote func register_player(info):
    # Get the id of the RPC sender.
    var id = get_tree().get_rpc_sender_id()
    # Store the info
    player_info[id] = info

    # Call function to update lobby UI here

You might have already noticed something different, which is the usage of the remote keyword on the register_player function:

remote func register_player(info):

このキーワードには主に2つの用途があります。 1つ目は、この関数をRPCから呼び出すことができることをGodotに知らせることです。キーワードが追加されていない場合、Godotはセキュリティのために関数を呼び出す試みをブロックします。これにより、セキュリティが非常に簡単になります(したがって、クライアントは別のクライアントのシステム上のファイルを削除する関数を呼び出すことができません)。

2番目の用途は、RPCを介して関数を呼び出す方法を指定することです。 4つの異なるキーワードがあります:

  • remote
  • remotesync
  • master
  • puppet

The remote keyword means that the rpc() call will go via network and execute remotely.

The remotesync keyword means that the rpc() call will go via network and execute remotely, but will also execute locally (do a normal function call).

The others will be explained further down. Note that you could also use the get_rpc_sender_id function on SceneTree to check which peer actually made the RPC call to register_player.

これにより、ロビー管理について多少説明する必要があります。ゲームを開始したら、セキュリティを追加して、クライアントがおかしなことをしないようにします(時々、またはゲーム開始前に送信する情報を検証します)。話を簡単にするため、また各ゲームは異なる情報を共有するため、ここでは具体例を示していません。

ゲームを開始する

十分な数のプレイヤーがロビーに集まったら、サーバーはおそらくゲームを開始するはずです。これ自体は特別なことではありませんが、この時点であなたの人生をもっと楽にするためにできるいくつかの素晴らしいトリックを説明します。

プレイヤーシーン

ほとんどのゲームでは、各プレイヤーが独自のシーンを持っているでしょう。これはマルチプレイヤーゲームであるため、すべてのピアで それに接続されているプレイヤーごとに1つのシーン をインスタンス化する必要があることに注意してください。 4プレイヤーゲームの場合、各ピアは4プレイヤーノードをインスタンス化する必要があります。

それでは、そのようなノードにどのように名前を付けるのでしょうか? Godotでは、ノードに一意の名前を付ける必要があります。また、プレイヤーがどのノードを各プレイヤーIDを表すかを比較的簡単に判断できる必要があります。

解決策は、インスタンス化されたプレイヤーシーンの ルートノードにnetwork ID という名前を付けるだけです。このように、それらはすべてのピアで同じになり、RPCはうまく機能します!次に例を示します:

remote func pre_configure_game():
    var selfPeerID = get_tree().get_network_unique_id()

    # Load world
    var world = load(which_level).instance()
    get_node("/root").add_child(world)

    # Load my player
    var my_player = preload("res://player.tscn").instance()
    my_player.set_name(str(selfPeerID))
    my_player.set_network_master(selfPeerID) # Will be explained later
    get_node("/root/world/players").add_child(my_player)

    # Load other players
    for p in player_info:
        var player = preload("res://player.tscn").instance()
        player.set_name(str(p))
        player.set_network_master(p) # Will be explained later
        get_node("/root/world/players").add_child(player)

    # Tell server (remember, server is always ID=1) that this peer is done pre-configuring.
    rpc_id(1, "done_preconfiguring", selfPeerID)

注釈

pre_configure_game()を実行するタイミングによっては、シーンの作成中に SceneTree がロックされているため(例: _ready が呼び出されている場合)、call_deferred() を介して遅延されるように add_child() への呼び出しを変更する必要がある場合があります。

ゲームの開始を同期する

プレイヤーのセットアップには、遅延、ハードウェアの違い、またはその他の理由により、ピアごとに異なる時間がかかる場合があります。全員の準備が整ったときにゲームが実際に開始されるようにするには、すべてのプレイヤーの準備が整うまでゲームを一時停止すると便利です:

remote func pre_configure_game():
    get_tree().set_pause(true) # Pre-pause
    # The rest is the same as in the code in the previous section (look above)

サーバーは、すべてのピアからOKを取得すると、たとえば次のように開始するように指示できます:

var players_done = []
remote func done_preconfiguring(who):
    # Here are some checks you can do, for example
    assert(get_tree().is_network_server())
    assert(who in player_info) # Exists
    assert(not who in players_done) # Was not added yet

    players_done.append(who)

    if players_done.size() == player_info.size():
        rpc("post_configure_game")

remote func post_configure_game():
    get_tree().set_pause(false)
    # Game starts now!

ゲームの同期

ほとんどのゲームでは、マルチプレイヤーネットワーキングの目標は、ゲームをプレイするすべてのピアで同期して実行することです。 RPCとリモートメンバー変数セットの実装を提供することに加えて、Godotはネットワークマスターの概念を追加します。

ネットワークマスター

ノードのネットワークマスターは、そのノードに対する最終的な権限を持つピアです。

明示的に設定しない場合、ネットワーク マスタは親ノードから継承され、変更されていない場合は常にサーバー(ID 1)になります。したがって、サーバーにはデフォルトですべてのノードに対する権限があります。

ネットワークマスターは、次の関数で設定できます Node.set_network_master(id, recursive) (デフォルトでは再帰は true で、ネットワークマスターは同様にノードのすべての子ノードに再帰的に設定されます)。

Node.is_network_master() を呼び出すと、ピア上の特定のノードインスタンスが、接続されているすべてのピアのこのノードのネットワークマスターであることを確認できます。これは、サーバーで実行されると true を返し、すべてのクライアントピアで false を返します。

前の例に注意を払っていた場合、各ピアがサーバーではなく独自の プレイヤー(ノード)のネットワークマスター権限を持つように設定されていることに気付くかもしれません:

[...]
# Load my player
var my_player = preload("res://player.tscn").instance()
my_player.set_name(str(selfPeerID))
my_player.set_network_master(selfPeerID) # The player belongs to this peer; it has the authority.
get_node("/root/world/players").add_child(my_player)

# Load other players
for p in player_info:
    var player = preload("res://player.tscn").instance()
    player.set_name(str(p))
    player.set_network_master(p) # Each other connected peer has authority over their own player.
    get_node("/root/world/players").add_child(player)
[...]

このコードが各ピアで実行されるたびに、ピアはそれが制御するノード上でマスターになり、他のすべてのノードはサーバがネットワーク マスターである Puppet として残ります。

明確にするために、これが bomber demo でどのように見えるかの例を示します:

../../_images/nmms.png

マスターとパペットのキーワード

The real advantage of this model is when used with the master/puppet keywords in GDScript (or their equivalent in C# and Visual Script). Similarly to the remote keyword, functions can also be tagged with them:

爆弾のコード例:

for p in bodies_in_area:
    if p.has_method("exploded"):
        p.rpc("exploded", bomb_owner)

プレイヤーのコード例:

puppet func stun():
    stunned = true

master func exploded(by_who):
    if stunned:
        return # Already stunned

    rpc("stun")
    stun() # Stun myself, could have used remotesync keyword too.

In the above example, a bomb explodes somewhere (likely managed by whoever is master). The bomb knows the bodies in the area, so it checks them and checks that they contain an exploded function.

If they do, the bomb calls exploded on it. However, the exploded method in the player has a master keyword. This means that only the player who is master for that instance will actually get the function.

This instance, then, calls the stun method in the same instances of that same player (but in different peers), and only those which are set as puppet, making the player look stunned in all the peers (as well as the current, master one).

Note that you could also send the stun() message only to a specific player by using rpc_id(<id>, "exploded", bomb_owner). This may not make much sense for an area-of-effect case like the bomb, but in other cases, like single target damage.

rpc_id(TARGET_PEER_ID, "stun") # Only stun the target peer