Up to date

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

高級多人遊戲

高階 API v.s. 低階 API

以下為 Godot 中高階與低階網路的區別,以及一些基礎說明。若想直接將網路加到第一個節點上,請跳至下方的 初始化網路 。但請記得等一下再回來讀剩下的部分!

Godot 使用 UDP、TCP、與其他如 SSL 與 HTTP 等的高級通訊協定來支援標準低階網路。這些通訊協定非常彈性,且幾乎可以用在任何地方。但是,要手動通過這些方法來同步遊戲狀態可能會需要大量的工作。有時候,這種工作是無法避免的,例如要處理後端的自定伺服器實作時。但大多數情況下,都建議考慮一下 Godot 的高階網路 API,使用高階網路 API 只需要犧牲小部分的細節控制,就可以換來更好的易用性。

由於繼承了低階通訊協定的一些功能,因此:

  • TCP確保封包始終可靠且按順序到達,但是由於錯誤校正,延遲通常更高。這也是一個非常複雜的協定,因為它了解什麼是“連接”,並最佳化了通常不適合用於多人遊戲的應用程式。封包被緩衝成更大的批次發送,從而減少了每份封包的開銷,提高了延遲。這對HTTP之類的東西可能很有用,但對遊戲通常不是。其中一些可以配置和禁用(例如禁用TCP連接的Nagle演算法)。

  • UDP是一種更簡單的協定,它僅發送封包(並且沒有“連接”的概念)。沒有錯誤糾正可以使其快速(低延遲),但是可能會丟失封包或以錯誤的順序接收封包。除此之外,UDP的MTU(最大封包大小)通常很低(只有幾百個位元組),因此傳輸較大的封包意味著得將其拆分、重組並在出現故障時重試。

普遍來說,TCP可以被認為是可靠,有序且緩慢;UDP不可靠,無序且快速。由於性能上的巨大差異,重新建構遊戲所需的TCP部分(可選的可靠性和封包順序),同時免去不必要的部分(擁塞/流量控制功能,Nagle演算法等)是合理的。因此,大多數遊戲引擎都提供了這樣的實作方式,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

警告

在您的遊戲中加入社交網路需要承擔一定的責任. 如果做錯了, 它會使您的應用程式很容易受到攻擊, 並可能導致欺騙或利用. 它甚至可能允許攻擊者破壞您的應用程式運作在的機器, 並使用您的伺服器發送垃圾郵件, 攻擊其他人或竊取您的使用者資料, 如果他們玩您的遊戲.

當涉及到網路而與Godot無關時, 情況總是如此. 當然, 您可以進行試驗, 但是在發行網路應用程式時, 請始終注意任何可能的安全問題.

中級抽象

在討論我們希望如何跨網路同步遊戲之前, 瞭解用於同步的基本網路API是如何工作的可能會有所幫助.

Godot使用了一個中層物件 NetworkedMultiplayerPeer. 這個物件並不是直接建立的, 但經過設計, 以便多個C++實作可以提供它.

這個物件擴充自 PacketPeer, 因此它繼承了所有用於序列化, 發送和接收資料的方法. 除此之外, 它還新增了設定節點, 傳輸模式等的方法. 它同時還包括當節點連接或斷開時將通知您的訊號.

這個類介面可以抽象出大多數型別的網路層, 拓撲結構和庫。預設情況下,Godot提供了一個基於ENet的實作( NetworkedMultiplayerEnet), 一個基於WebRTC的實作( WebRTCMultiplayer), 還有一個基於WebSocket的實作( WebSocketMultiplayerPeer),但這可以用來實作移動API(用於特設的WiFi, 藍牙)或自訂裝置/控制台特定的網路API。

大多數常見情況下, 不鼓勵直接使用這個物件, 因為Godot提供了更高級別的網路使用. 只有當遊戲對較低級別的API有特殊需求的情況下, 才使用它.

其他建議

託管伺服器時,LAN(區域網路)上的使用者端可以使用內部 IP 位址進行連接,該位址通常採用「192.168.*.*」形式。非 LAN/Internet 使用者端**無法**存取此內部 IP 位址。

在 Windows 上,您可以透過開啟命令提示字元並輸入「ipconfig」來找到您的內部 IP 位址。在 macOS 上,開啟終端機並輸入「ifconfig」。在 Linux 上,打開終端機並輸入“ip addr”。

如果您在自己的電腦上託管伺服器並希望非 LAN 使用者端連接到它,您可能必須「轉送」路由器上的伺服器連接埠。這是讓您的伺服器可以透過網路存取所必需的,因為大多數住宅連線都使用「NAT <https://en.wikipedia.org/wiki/Network_address_translation>」__。 Godot 的高級多人 API 僅使用 UDP,因此您必須以 UDP 轉發端口,而不僅僅是 TCP。

轉送 UDP 連接埠並確保您的伺服器使用該連接埠後,您可以使用「此網站 <https://icanhazip.com/>」來尋找您的公用 IP 位址。然後將此公用 IP 位址提供給任何希望連接到您的伺服器的 Internet 使用者端。

Godot 的高級多人 API 使用 ENet 的修改版本,它允許完全支援 IPv6。

初始化網路

在Godot中, 控制聯網的物件與控制所有與樹相關的東西的物件是相同的: SceneTree.

每個節點都有一個「multiplayer」屬性,它是對場景樹為其配置的「MultiplayerAPI」實例的參考。最初,每個節點都配置相同的預設「MultiplayerAPI」物件。

可以建立一個新的“MultiplayerAPI”物件並將其分配給場景樹中的“NodePath”,這將覆蓋該路徑及其所有後代節點的“multiplayer”。這允許兄弟節點配置不同的對等點,從而可以在一個 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.

要初始化網路,必須建立一個「MultiplayerPeer」對象,將其初始化為伺服器或客戶端,然後傳遞給「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

停止聯網功能:

multiplayer.multiplayer_peer = null

警告

當匯出到 Android 時,在匯出專案或使用一鍵部署之前,確保在 Android 匯出預設中啟用 INTERNET 許可權。否則,任何形式的網路通信都會被 Android 系統阻止。

管理連接

每個對等點都被分配了一個唯一的 ID。伺服器的 ID 始終為 1,並且為客戶端分配一個隨機正整數。

透過連接到「MultiplayerAPI」的訊號可以回應連接或斷開連接:

  • peer_connected(id: int) 此訊號在每個其他對等點上與新連接的對等點ID 一起發出,並在新對等點上多次發出,其中一次與每個其他對等點ID 一起發出。

  • peer_disconnected(id: int) 當一個對等點斷開連接時,該訊號會在每個剩餘對等點上發出。

有下列事項需注意:

  • connected_to_server()

  • connection_failed()

  • server_disconnected()

若要取得關聯對等點的唯一 ID:

multiplayer.get_unique_id()

檢查樹是否被初始化為伺服器或使用者端:

multiplayer.is_server()

遠程程式呼叫

遠端過程呼叫(RPC)是可以在其他對等點上呼叫的函式。若要建立一個,請在函式定義之前使用“@rpc”註解。若要呼叫 RPC,請使用 Callable 的方法 rpc() 來呼叫每個對等點,或使用 rpc_id() 來呼叫特定對等點。

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.")

RPC 不會序列化物件或可呼叫物件。

為了使遠端呼叫成功,發送節點和接收節點需要具有相同的“NodePath”,這意味著它們必須具有相同的名稱。當對預期使用 RPC 的節點使用「add_child()」時,請將參數「force_read_name」設為「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), and both must have the same signature, even if this function is not currently used.

If these conditions are not fulfilled, the script may print an error or cause unwanted behavior. See further explanation and troubleshooting on this post.

註解可以採用多個參數,這些參數具有預設值。 @rpc 相當於:

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

常見的使用情景如下:

mode:

  • 『「authority」『:只有多人權限(伺服器)才能遠端呼叫。

  • “any_peer”:允許客戶端遠端呼叫。對於傳輸使用者輸入很有用。

sync:

  • 注意:如果引擎當機或無回應,該函式不會被呼叫。

  • “call_local”:可以在本地對等點上呼叫函式。當伺服器同時也是玩家時很有用。

transfer_mode:

  • 「不可靠」 封包未被確認,可能會遺失,並且可能以任何順序到達。

  • 「unreliable_ordered」 封包按照發送的順序接收。這是透過忽略稍後到達的封包(如果已經收到在它們之後發送的另一個封包)來實作的。如果使用不當可能會導致丟包。

  • 「可靠」 發送重新傳送嘗試,直到封包被確認為止,並且它們的順序被保留。具有顯著的性能損失。

transfer_channel 是頻道索引。

前 3 個可以按任何順序傳遞,但「transfer_channel」必須始終是最後一個。

當在 rpc 呼叫的函式中使用時,函式「multiplayer.get_remote_sender_id()」可用於取得 rpc 發送者的唯一 ID。

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.

改動

現代網路協定支援通道,通道是連接內的單獨連接。這允許多個封包流互不干擾。

例如,遊戲聊天相關訊息和一些核心遊戲訊息都應該可靠地發送,但遊戲訊息不應等待聊天訊息被確認。這可以透過使用不同的管道來實作。

當與不可靠的有序傳輸模式一起使用時,通道也很有用。使用此傳輸模式發送可變大小的封包可能會導致封包遺失,因為到達較慢的封包將被忽略。透過使用通道將它們分成多個同質封包流,可以實作有序傳輸,且封包遺失很少,並且不會因可靠模式而導致延遲損失。

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

實作

這是一個範例大廳,可以處理同伴的加入和離開,透過訊號通知UI場景,並在所有客戶端載入遊戲場景後啟動遊戲。

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

遊戲場景的根節點應命名為 Game。在附加的腳本中:

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的專用伺服器上運作. 參見 為專用伺服器匯出 獲取更多資訊.

備註

這個頁面上的程式碼樣本並不是為了在專用伺服器上運作而設計的. 必須修改它們, 使伺服器不被認為是一個玩家, 還必須修改遊戲啟動機制, 使第一個加入的玩家可以啟動遊戲.