High-level multiplayer

High-level vs low-level API

下面解释了Godot高级、低级网络的区别以及一些基本原理。如果您等不及了且将网络添加到您的第一个节点中,请跳到下面的 初始化网络 。但是请确保稍后阅读其余部分!

Godot始终支持通过UDP、TCP 和一些更高级别的协议(如SSL和HTTP)进行标准的低级网络连接。这些协议非常灵活,几乎可以用于任何事情。然而,使用它们来手动同步游戏状态可能需要大量的工作。有时这种工作是无法避免的,或者是值得的,例如在后台使用自定义服务器实现时。但在大多数情况下,考虑Godot的高级网络API是值得的,它牺牲了对低级网络的一些细粒度控制,以获得更大的易用性。

这是由于低级协议的固有限制:

  • TCP确保包总是可靠地、有序地到达,但是由于错误纠正,延迟通常更高。它也是一个相当复杂的协议,因为它理解什么是``连接``,并针对通常不适合多人游戏等应用程序的目标进行优化。包被缓冲成更大的批发送,每包开销更少,延迟更高。这对于HTTP之类的东西可能很有用,但对于游戏通常不太有用。其中一些可以配置和禁用(例如禁用TCP连接的 "Nagle's algorithm" )。
  • UDP 是一个更简单的协议,它只发送数据包(没有“连接”的概念)。没有错误纠正使其非常快(低延迟),但是包可能在过程中丢失或以错误的顺序接收。此外,UDP 的 MTU (最大数据包大小)通常很低(只有几百字节),因此传输更大的数据包意味着对它们进行分割、重新组织它们,并在部分失败时重试。

一般来说,TCP 可以被认为是可靠的、有序的和缓慢的; UDP则是不可靠,无序, 但是速度快。由于性能上的巨大差异,在避免不需要的部分(拥塞/流量控制特性、Nagle算法等)的同时,重新构建游戏所需的TCP部分(可选的可靠性和包顺序)通常是合理的。正因为如此,大多数游戏引擎都带有这样的实现,Godot也不例外。

综上所述,您可以使用低级网络API来实现最大限度的控制,并在完全裸露的网络协议之上实现所有功能,或者使用基于 SceneTree 的高级API,后者以通常以一种比较优化的方式在后台完成大部分繁重的工作。

注解

Godot支持的大多数平台都提供所有或大部分上述的高、低网络功能。但是,由于网络在很大程度上依赖于硬件和操作系统,一些特性可能会改变,或者在某些目标平台上不可用。最值得注意的是,HTML5平台目前只提供WebSocket支持,缺乏一些高级功能,以及对TCP和UDP等低级协议的原始访问。

注解

More about TCP/IP, UDP, and networking: 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 C++ implementations can provide it.

这个对象扩展自 PacketPeer,因此它继承了所有用于序列化、发送和接收数据的方法。除此之外,它还添加了设置节点、传输模式等的方法。它同时还包括当节点连接或断开时将通知您的信号。

这个类接口可以表示大多数类型的网络层、拓扑结构和库。默认情况下,Godot提供基于ENet的实现(NetworkedMultiplayerEnet),但是这也可以用于实现手机的API(针对专用WiFi、蓝牙)或自定义设备/特定的控制台的网络API。

大多数常见情况下,不鼓励直接使用这个对象,因为Godot提供了更高级别的网络使用。只有当游戏对较低级别的API有特殊需求的情况下,才使用它。

初始化网络

在Godot中, 控制联网的对象与控制所有与树相关的东西的对象是相同的: SceneTree

To initialize high-level networking, the SceneTree must be provided a NetworkedMultiplayerPeer object.

To create that object, it first has to be initialized as a server or client.

作为服务器初始化,监听给定的端口,指定最大节点的数量:

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

作为客户端初始化,连接到给定的IP和端口:

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

Get the previously set network peer:

get_tree().get_network_peer()

检查树是否被初始化为服务器或客户端:

get_tree().is_network_server()

停止联网功能:

get_tree().network_peer = null

(更加合理的做法是, 首先发送消息让其他节点知道您正在离开,而不是直接让连接关闭或让连接超时,但这也取决于您的游戏设计。)

管理连接

有些游戏在任何时候都可以接受连接,也有游戏只在大厅阶段接受连接。可以请求Godot在任何时间点不再接受连接(参见 set_refuse_new_network_connections(bool)SceneTree 的相关方法)。为了管理连接的节点,Godot在SceneTree中提供了以下信号:

服务器和客户端:

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

The above signals are called on every peer connected to the server (including on the server) when a new peer connects or disconnects. Clients will connect with a unique ID greater than 1, while network peer ID 1 is always the server. Anything below 1 should be handled as invalid. You can retrieve the ID for the local system via SceneTree.get_network_unique_id(). These IDs will be useful mostly for lobby management and should generally be stored, as they identify connected peers and thus players. You can also use IDs to send messages only to certain peers.

客户端:

  • connected_to_server
  • connection_failed
  • server_disconnected

Again, all these functions are mainly useful for lobby management or for adding/removing players on the fly. For these tasks, the server clearly has to work as a server and you have to perform tasks manually such as sending a newly connected player information about other already connected players (e.g. their names, stats, etc).

您可以用任何您想要的方式实现大厅,但是最常见的方式是用一个在所有游戏角色的场景中具有相同名字的节点。通常,一个自动加载的节点/单例非常适合于此,这样就可以在任何时候访问它,例如``/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)

可以用两种方式来调用函数:

  • 可靠的:函数调用无论如何都会到达,但是可能需要更长的时间,因为在发生故障时它将被重新发送。
  • Unreliable: if the function call does not arrive, it will not be re-transmitted; but if it arrives, it will do it quickly.

在大多数情况下,需要可靠的调用。当同步对象位置时,不可靠的调用才很有用(因为同步必须持续发生,如果包丢失,这并不那么糟糕,因为新的包最终会到达;同时包很可能会过时,因为对象在此期间进一步移动了,即使它被可靠地怨恨)。

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

This keyword has two main uses. The first is to let Godot know that this function can be called from RPC. If no keywords are added, Godot will block any attempts to call functions for security. This makes security work a lot easier (so a client can't call a function to delete a file on another client's system).

第二个用途是指定如何通过RPC调用该函数。这里又有四个不同的关键字:

  • 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.

With this, lobby management should be more or less explained. Once you have your game going, you will most likely want to add some extra security to make sure clients don't do anything funny (just validate the info they send from time to time, or before game start). For the sake of simplicity and because each game will share different information, this is not shown here.

开始游戏

一旦有足够的游戏角色聚集在大厅时,服务器应该开始游戏。这本身没有什么特别的,但是我们将解释一些很好的技巧,这些技巧可以在这点上让您的生活更容易。

游戏角色场景

在大多数游戏中,每个游戏角色都可能有自己的场景。请记住,这是一个多人游戏,所以在每个客户端中,您需要为连接到它的每个游戏角色实例化 一个场景 。对于一个4人游戏,每个客户端需要4个游戏角色节点实例。

So, how to name such nodes? In Godot, nodes need to have a unique name. It must also be relatively easy for a player to tell which node represents each player ID.

解决方案是简单地将 实例化后的游戏角色场景的根节点命名为它的网络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.
    # The server can call get_tree().get_rpc_sender_id() to find out who said they were done.
    rpc_id(1, "done_preconfiguring")

注解

Depending on when you execute pre_configure_game(), you may need to change any calls to add_child() to be deferred via call_deferred(), as the SceneTree is locked while the scene is being created (e.g. when _ready() is being called).

同步游戏开始

由于延迟、不同的硬件或其他原因,设置游戏角色在每个客户端上花费的时间可能不同。为了确保游戏确实会在每个人都准备好的时候开始,有必要暂停游戏,直到所有的游戏角色都准备好:

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():
    var who = get_tree().get_rpc_sender_id()
    # 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():
    # Only the server is allowed to tell a client to unpause
    if 1 == get_tree().get_rpc_sender_id():
        get_tree().set_pause(false)
        # Game starts now!

同步游戏

In most games, the goal of multiplayer networking is that the game runs synchronized on all the peers playing it. Besides supplying an RPC and remote member variable set implementation, Godot adds the concept of network masters.

网络主人

一个节点的网络主人是对该节点具有终极权限的客户端。

When not explicitly set, the network master is inherited from the parent node, which if not changed, is always going to be the server (ID 1). Thus the server has authority over all nodes by default.

The network master can be set with the function Node.set_network_master(id, recursive) (recursive is true by default and means the network master is recursively set on all child nodes of the node as well).

Checking that a specific node instance on a peer is the network master for this node for all connected peers is done by calling Node.is_network_master(). This will return true when executed on the server and false on all client peers.

If you have paid attention to the previous example, it's possible you noticed that each peer was set to have network master authority for their own player (Node) instead of the server:

[...]
# 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)
[...]

每当在客户端上执行这段代码时,客户端就使得它控制的节点上成为主人,同时其他所有节点仍然保持为傀儡(服务器是它们的网络主人)。

为了阐明这点,可以看看这个 轰炸机演示 :

../../_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 this player instance for myself as well; could instead have used
    # the remotesync keyword above (in place of puppet) to achieve this.
    stun()

In the above example, a bomb explodes somewhere (likely managed by whoever is the master of this bomb-node, e.g. the host). The bomb knows the bodies (player nodes) in the area, so it checks that they contain an exploded method before calling it.

Recall that each peer has a complete set of instances of player nodes, one instance for each peer (including itself and the host). Each peer has set itself as the master of the instance corresponding to itself, and it has set a different peer as the master for each of the other instances.

Now, going back to the call to the exploded method, the bomb on the host has called it remotely on all bodies in the area that have the method. However, this method is in a player node and has a master keyword.

The master keyword on the exploded method in the player node means two things for how this call is made. Firstly, from the perspective of the calling peer (the host), the calling peer will only attempt to remotely call the method on the peer that it has set as the network master of the player node in question. Secondly, from the perspective of the peer the host is sending the call to, the peer will only accept the call if it set itself as the network master of the player node with the method being called (which has the master keyword). This works well as long as all peers agree on who is the master of what.

The above setup means that only the peer who owns the affected body will be responsible for telling all the other peers that its body was stunned, after being remotely instructed to do so by the host's bomb. The owning peer therefore (still in the exploded method) tells all the other peers that its player node was stunned. The peer does this by remotely calling the stun method on all instances of that player node (on the other peers). Because the stun method has the puppet keyword, only peers who did not set themselves as the network master of the node will call it (in other words, those peers are set as puppets for that node by virtue of not being the network master of it).

The result of this call to stun is to make the player look stunned on the screen of all the peers, including the current network master peer (due to the local call to stun after rpc("stun")).

The master of the bomb (the host) repeats the above steps for each of the bodies in the area, such that all the instances of any player in the bomb area get stunned on the screens of all the peers.

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 might in other cases, like single target damage.

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

Exporting for dedicated servers

Once you've made a multiplayer game, you may want to export it to run it on a dedicated server with no GPU available. See Exporting for dedicated servers for more information.

注解

The code samples on this page aren't designed to run on a dedicated server. You'll have to modify them so the server isn't considered to be a player. You'll also have to modify the game starting mechanism so that the first player who joins can start the game.

注解

The bomberman example here is largely for illustrational purposes, and does not do anything on the host-side to handle the case where a peer uses a custom client to cheat by for example refusing to to stun itself. In the current implementation such cheating is perfectly possible because each client is the network master of its own player, and the network master of a player is the one which decides whether to call the I-was-stunned method (stun) on all of the other peers and itself.