Up to date

This page is up to date for Godot 4.2. If you still find outdated information, please open an issue.

高级多人游戏

高层与底层 API

下面解释了 Godot 高阶、低阶网络的区别以及一些基本原理。如果你想一头扎进去,直接为你的最初的节点添加网络功能,请跳到下面的初始化网络。但是请确保稍后阅读其余部分!

Godot always supported standard low-level networking via UDP, TCP and some higher-level protocols such as HTTP and SSL. These protocols are flexible and can be used for almost anything. However, using them to synchronize game state manually can be a large amount of work. Sometimes that work can't be avoided or is worth it, for example when working with a custom server implementation on the backend. But in most cases, it's worthwhile to consider Godot's high-level networking API, which sacrifices some of the fine-grained control of low-level networking for greater ease of use.

这是底层协议的固有限制所造成的:

  • TCP 能够确保数据包总是可靠、有序地到达,但是由于错误纠正,延迟通常更高。它也是一个相当复杂的协议,因为它理解什么是“连接”,它优化的目标也经常不是多人游戏这种应用程序。数据包会被缓冲成更大的批次发送,用更高的延迟来交换更小的单数据包开销。这对于 HTTP 之类的东西可能很有用,但对于游戏通常不太有用。其中一些可以配置和禁用(例如禁用 TCP 连接的“Nagle 算法”)。

  • UDP 是一个更简单的协议,它只发送数据包(没有“连接”的概念)。因为没有错误纠正,所以非常快(低延迟),但数据包就可能在发生丢失或以错误的顺序接收的情况。此外,UDP 的 MTU(Maximum Packet Size,最大数据包大小)通常很低(只有几百字节),因此传输更大的数据包意味着需要对它们进行分割、重新组织、某一部分失败时还要进行重试。

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

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

备注

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

备注

更多关于TCP/IP, UDP和网络的信息: https://gafferongames.com/post/udp_vs_tcp/

Gaffer On Games有很多关于游戏中网络的有用文章( 这里 ), 包括全面的 游戏中的网络模型介绍 .

如果你想使用你选择的底层网络库来代替Godot的内置网络, 请参阅这里的示例 : https://github.com/PerduGames/gdnet3

警告

Adding networking to your game comes with some responsibility. It can make your application vulnerable if done wrong and may lead to cheats or exploits. It may even allow an attacker to compromise the machines your application runs on and use your servers to send spam, attack others or steal your users' data if they play your game.

当涉及到网络而与Godot无关时, 情况总是如此. 当然, 你可以进行试验, 但是在发布网络应用程序时, 请始终注意任何可能的安全问题.

中级抽象

在讨论我们希望如何跨网络同步游戏之前, 了解用于同步的基本网络API是如何工作的可能会有所帮助.

Godot uses a mid-level object MultiplayerPeer. This object is not meant to be created directly, but is designed so that several C++ 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 (ENetMultiplayerPeer), one based on WebRTC (WebRTCMultiplayerPeer), and one based on WebSocket (WebSocketPeer), but this could be used to implement mobile APIs (for ad hoc WiFi, Bluetooth) or custom device/console-specific networking APIs.

For most common cases, using this object directly is discouraged, as Godot provides even higher level networking facilities. This object is still made available in case a game has specific needs for a lower-level API.

托管注意事项

When hosting a server, clients on your LAN can connect using the internal IP address which is usually of the form 192.168.*.*. This internal IP address is not reachable by non-LAN/Internet clients.

On Windows, you can find your internal IP address by opening a command prompt and entering ipconfig. On macOS, open a Terminal and enter ifconfig. On Linux, open a terminal and enter ip addr.

If you're hosting a server on your own machine and want non-LAN clients to connect to it, you'll probably have to forward the server port on your router. This is required to make your server reachable from the Internet since most residential connections use a NAT. Godot's high-level multiplayer API only uses UDP, so you must forward the port in UDP, not just TCP.

After forwarding an UDP port and making sure your server uses that port, you can use this website to find your public IP address. Then give this public IP address to any Internet clients that wish to connect to your server.

Godot's high-level multiplayer API uses a modified version of ENet which allows for full IPv6 support.

初始化网络

High level networking in Godot is managed by the SceneTree.

Each node has a multiplayer property, which is a reference to the MultiplayerAPI instance configured for it by the scene tree. Initially, every node is configured with the same default MultiplayerAPI object.

It is possible to create a new MultiplayerAPI object and assign it to a NodePath in the the scene tree, which will override multiplayer for the node at that path and all of its descendants. This allows sibling nodes to be configured with different peers, which makes it possible to run a server and a client simultaneously in one instance of Godot.

# By default, these expressions are interchangeable.
multiplayer # Get the MultiplayerAPI object configured for this node.
get_tree().get_multiplayer() # Get the default MultiplayerAPI object.

To initialize networking, a MultiplayerPeer object must be created, initialized as a server or client, and passed to the MultiplayerAPI.

# Create client.
var peer = ENetMultiplayerPeer.new()
peer.create_client(IP_ADDRESS, PORT)
multiplayer.multiplayer_peer = peer

# Create server.
var peer = ENetMultiplayerPeer.new()
peer.create_server(PORT, MAX_CLIENTS)
multiplayer.multiplayer_peer = peer

To terminate networking:

multiplayer.multiplayer_peer = null

警告

当导出到 Android 时,在导出项目或使用一键部署之前,确保在 Android 导出预设中启用 INTERNET 权限。否则,任何形式的网络通信都会被 Android 系统阻止。

管理连接

Every peer is assigned a unique ID. The server's ID is always 1, and clients are assigned a random positive integer.

Responding to connections or disconnections is possible by connecting to MultiplayerAPI's signals:

  • peer_connected(id: int) This signal is emitted with the newly connected peer's ID on each other peer, and on the new peer multiple times, once with each other peer's ID.

  • peer_disconnected(id: int) This signal is emitted on every remaining peer when one disconnects.

The rest are only emitted on clients:

  • connected_to_server()

  • connection_failed()

  • server_disconnected()

To get the unique ID of the associated peer:

multiplayer.get_unique_id()

To check whether the peer is server or client:

multiplayer.is_server()

远程过程调用

Remote procedure calls, or RPCs, are functions that can be called on other peers. To create one, use the @rpc annotation before a function definition. To call an RPC, use Callable's method rpc() to call in every peer, or rpc_id() to call in a specific peer.

func _ready():
    if multiplayer.is_server():
        print_once_per_client.rpc()

@rpc
func print_once_per_client():
    print("I will be printed to the console once per each connected client.")

RPCs will not serialize objects or callables.

For a remote call to be successful, the sending and receiving node need to have the same NodePath, which means they must have the same name. When using add_child() for nodes which are expected to use RPCs, set the argument force_readable_name to true.

警告

If a function is annotated with @rpc on the client script (resp. server script), then this function must also be declared on the server script (resp. client script). Both RPCs must have the same signature which is evaluated with a checksum of all RPCs. All RPCs in a script are checked at once, and all RPCs must be declared on both the client scripts and the server scripts, even functions that are currently not in use.

The signature of the RPC includes the @rpc() declaration, the function, return type, AND the nodepath. If an RPC resides in a script attached to /root/Main/Node1, then it must reside in precisely the same path and node on both the client script and the server script. Function arguments (example: func sendstuff(): and func sendstuff(arg1, arg2): will pass signature matching).

If these conditions are not met (if all RPCs do not pass signature matching), the script may print an error or cause unwanted behavior. The error message may be unrelated to the RPC function you are currently building and testing.

See further explanation and troubleshooting on this post.

The annotation can take a number of arguments, which have default values. @rpc is equivalent to:

@rpc("authority", "call_remote", "unreliable", 0)

The parameters and their functions are as follows:

mode

  • "authority": Only the multiplayer authority (the server) can call remotely.

  • "any_peer": Clients are allowed to call remotely. Useful for transferring user input.

remotesync

  • "call_remote": The function will not be called on the local peer.

  • "call_local": The function can be called on the local peer. Useful when the server is also a player.

transform

  • "unreliable" Packets are not acknowledged, can be lost, and can arrive at any order.

  • "unreliable_ordered" Packets are received in the order they were sent in. This is achieved by ignoring packets that arrive later if another that was sent after them has already been received. Can cause packet loss if used incorrectly.

  • "reliable" Resend attempts are sent until packets are acknowledged, and their order is preserved. Has a significant performance penalty.

transfer_channel is the channel index.

The first 3 can be passed in any order, but transfer_channel must always be last.

The function multiplayer.get_remote_sender_id() can be used to get the unique id of an rpc sender, when used within the function called by rpc.

func _on_some_input(): # Connected to some input.
    transfer_some_input.rpc_id(1) # Send the input only to the server.


# Call local is required if the server is also a player.
@rpc("any_peer", "call_local", "reliable")
func transfer_some_input():
    # The server knows who sent the input.
    var sender_id = multiplayer.get_remote_sender_id()
    # Process the input and affect game logic.

通道

Modern networking protocols support channels, which are separate connections within the connection. This allows for multiple streams of packets that do not interfere with each other.

For example, game chat related messages and some of the core gameplay messages should all be sent reliably, but a gameplay message should not wait for a chat message to be acknowledged. This can be achieved by using different channels.

Channels are also useful when used with the unreliable ordered transfer mode. Sending packets of variable size with this transfer mode can cause packet loss, since packets which are slower to arrive are ignored. Separating them into multiple streams of homogeneous packets by using channels allows ordered transfer with little packet loss, and without the latency penalty caused by reliable mode.

The default channel with index 0 is actually three different channels - one for each transfer mode.

大厅的示例实现

This is an example lobby that can handle peers joining and leaving, notify UI scenes through signals, and start the game after all clients have loaded the game scene.

extends Node

# Autoload named Lobby

# These signals can be connected to by a UI lobby scene or the game scene.
signal player_connected(peer_id, player_info)
signal player_disconnected(peer_id)
signal server_disconnected

const PORT = 7000
const DEFAULT_SERVER_IP = "127.0.0.1" # IPv4 localhost
const MAX_CONNECTIONS = 20

# This will contain player info for every player,
# with the keys being each player's unique IDs.
var players = {}

# This is the local player info. This should be modified locally
# before the connection is made. It will be passed to every other peer.
# For example, the value of "name" can be set to something the player
# entered in a UI scene.
var player_info = {"name": "Name"}

var players_loaded = 0



func _ready():
    multiplayer.peer_connected.connect(_on_player_connected)
    multiplayer.peer_disconnected.connect(_on_player_disconnected)
    multiplayer.connected_to_server.connect(_on_connected_ok)
    multiplayer.connection_failed.connect(_on_connected_fail)
    multiplayer.server_disconnected.connect(_on_server_disconnected)


func join_game(address = ""):
    if address.is_empty():
        address = DEFAULT_SERVER_IP
    var peer = ENetMultiplayerPeer.new()
    var error = peer.create_client(address, PORT)
    if error:
        return error
    multiplayer.multiplayer_peer = peer


func create_game():
    var peer = ENetMultiplayerPeer.new()
    var error = peer.create_server(PORT, MAX_CONNECTIONS)
    if error:
        return error
    multiplayer.multiplayer_peer = peer

    players[1] = player_info
    player_connected.emit(1, player_info)


func remove_multiplayer_peer():
    multiplayer.multiplayer_peer = null


# When the server decides to start the game from a UI scene,
# do Lobby.load_game.rpc(filepath)
@rpc("call_local", "reliable")
func load_game(game_scene_path):
    get_tree().change_scene_to_file(game_scene_path)


# Every peer will call this when they have loaded the game scene.
@rpc("any_peer", "call_local", "reliable")
func player_loaded():
    if multiplayer.is_server():
        players_loaded += 1
        if players_loaded == players.size():
            $/root/Game.start_game()
            players_loaded = 0


# When a peer connects, send them my player info.
# This allows transfer of all desired data for each player, not only the unique ID.
func _on_player_connected(id):
    _register_player.rpc_id(id, player_info)


@rpc("any_peer", "reliable")
func _register_player(new_player_info):
    var new_player_id = multiplayer.get_remote_sender_id()
    players[new_player_id] = new_player_info
    player_connected.emit(new_player_id, new_player_info)


func _on_player_disconnected(id):
    players.erase(id)
    player_disconnected.emit(id)


func _on_connected_ok():
    var peer_id = multiplayer.get_unique_id()
    players[peer_id] = player_info
    player_connected.emit(peer_id, player_info)


func _on_connected_fail():
    multiplayer.multiplayer_peer = null


func _on_server_disconnected():
    multiplayer.multiplayer_peer = null
    players.clear()
    server_disconnected.emit()

The game scene's root node should be named Game. In the script attached to it:

extends Node3D # Or Node2D.



func _ready():
    # Preconfigure game.

    Lobby.player_loaded.rpc_id(1) # Tell the server that this peer has loaded.


# Called only on the server.
func start_game():
    # All peers are ready to receive RPCs in this scene.

为专用服务器导出

一旦你制作了一个多人游戏, 你可能会想导出它到一个没有GPU的专用服务器上运行. 参见 为专用服务器导出 获取更多信息.

备注

这个页面上的代码样本并不是为了在专用服务器上运行而设计的. 必须修改它们, 使服务器不被认为是一个玩家, 还必须修改游戏启动机制, 使第一个加入的玩家可以启动游戏.