Attention: Here be dragons

This is the latest (unstable) version of this documentation, which may document features not available in or compatible with released stable versions of Godot.

高階多人連線

高階 API 與 低階 API

以下說明 Godot 中高階和低階網路的差異及其基本原理。如果你想直接開始在節點中加入網路功能,可直接跳到下方的 初始化網路 章節,但建議稍後還是回來閱讀其餘內容!

Godot 一直以來都支援標準的低階網路通訊協定,如 UDP(使用者資料包協定)TCP(傳輸控制協定),以及更高階的 HTTP(超文本傳輸協定)SSL(安全通訊層) 等。這些協定非常彈性,幾乎可以用於任何場合。不過,若要手動透過這些協定來同步遊戲狀態,會需要投入相當多的開發工作。有時這是必要或值得的(例如你在後端使用自訂伺服器實作時);但大多數情況下,建議優先考慮 Godot 的高階網路 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 網站有許多實用的遊戲網路技術文章(連結),例如這一篇超完整的《遊戲網路架構簡介 <https://gafferongames.com/post/what_every_programmer_needs_to_know_about_game_networking/>`__》。

警告

在遊戲中加入網路功能也意味著更多責任。如果處理不當,應用程式可能會有安全漏洞,導致被外掛、作弊、甚至被利用。更嚴重時,攻擊者可能入侵你的伺服器,拿來發送垃圾信、攻擊他人,或竊取玩家資料。

只要是網路應用都會面臨這些問題,這與 Godot 本身無關。你可以隨意測試、練習,但只要要公開發佈網路遊戲,就務必重視安全性問題。

中階抽象

在了解如何跨網路同步遊戲之前,有必要先認識 Godot 用來同步的底層網路 API 怎麼運作。

Godot 採用了一個中階物件 MultiplayerPeer。這個物件並不是讓你直接建立,而是由多種 C++ 實作類別提供。

此物件繼承自 PacketPeer,因此擁有序列化、發送與接收資料的所有方便方法。此外,也有設定節點、傳輸模式等方法,還有用於偵測連線/斷線的 signals。

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 (WebSocketMultiplayerPeer), but this could be used to implement mobile APIs (for ad hoc WiFi, Bluetooth) or custom device/console-specific networking APIs.

在大多數情況下,建議不要直接用這個物件,因為 Godot 還有更高階的網路 API。只有當你真的需要低階控制時才用它。

主機託管須知

在主機託管伺服器時,同一 區域網路(LAN) 內的用戶端可以用內部 IP(通常是 192.168.*.* )連線。但這個內部 IP 不能被外部網際網路上的玩家存取。

在 Windows 上,可在命令提示字元輸入 ipconfig 查詢;macOS 請於終端機輸入 ifconfig;Linux 則輸入 ip addr。

如果你在自己電腦上架設伺服器,並希望讓外部(非區網)用戶端連線,通常必須在家用路由器上「連接埠轉發」伺服器的埠號。這是因為大多數家用連線都用 NAT(https://en.wikipedia.org/wiki/Network_address_translation)。Godot 高階多人 API 只用 UDP,所以你必須設定 UDP 連接埠轉發,不只是 TCP。

完成 UDP 連接埠轉發並確認伺服器使用該埠號後,可透過「https://icanhazip.com/」查詢你的公開 IP,然後將此 IP 提供給想連線的網路玩家。

Godot 的高階多人 API 採用改寫版的 ENet,支援完整 IPv6。

初始化網路

在 Godot 中,高階網路功能是由 SceneTree 負責管理的。

每個節點都有一個 multiplayer 屬性,這是一個由場景樹配置的 MultiplayerAPI 實例的參考。預設情況下,所有節點都指向同一個 MultiplayerAPI 物件。

你可以建立新的 MultiplayerAPI 物件並指定到場景樹中的某個 NodePath,如此該路徑上的節點與其所有子孫節點的 multiplayer 會被覆寫。這讓同層節點可配置不同的 peer,進而能在同一個 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 = OfflineMultiplayerPeer.new()

警告

匯出到 Android 時,請在匯出專案或一鍵部署前,於 Android 匯出設定啟用 INTERNET 權限,否則所有網路連線都會被 Android 阻擋。

管理連線

每個連線節點(peer)都會被指定一個唯一 ID。伺服器的 ID 永遠是 1,客戶端會分配隨機正整數 ID。

可以藉由連接 MultiplayerAPI 的 signals 來監聽連線與斷線事件:

  • peer_connected(id: int) 這個 signal 會在所有已連線 peer 上觸發,參數是新連線 peer 的 ID;新加入的 peer 則會收到一次所有其他 peer 的 ID。

  • peer_disconnected(id: int) 這個 signal 會在其他所有 peer 上觸發,參數是剛剛斷線的 peer ID。

下列 signals 僅會在客戶端觸發:

  • connected_to_server()

  • connection_failed()

  • server_disconnected()

取得目前 peer 的唯一 ID:

multiplayer.get_unique_id()

檢查目前 peer 是伺服器還是客戶端:

multiplayer.is_server()

遠端程序呼叫(RPC)

遠端程序呼叫(RPC)是可以在其他 peer 執行的函式。要定義 RPC,請在函式前加 @rpc 標註。呼叫 RPC 時,使用 rpc() 會在所有 peer 執行,使用 rpc_id() 則只會指定 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.

要讓遠端呼叫成功,發送端與接收端節點的 NodePath 必須完全相同,也就是節點名稱必須一致。若是使用 add_child() 加入會用到 RPC 的節點,建議將 force_readable_name 參數設為 true

警告

若某函式在客戶端(或伺服器端)腳本用 @rpc 標註,則必須在伺服器端(或客戶端)腳本也宣告同名函式。所有 RPC 必須有相同簽名,這會以**所有 RPC** 計算檢查碼。所有 RPC 會一次性檢查,且必須在客戶端與伺服器端腳本都宣告,即使該函式當下未被使用也一樣

RPC 的簽名包含 @rpc() 宣告、函式名稱、回傳型別,以及 NodePath。如果 RPC 函式是在 /root/Main/Node1 這個節點上,那無論在客戶端還是伺服器端,都必須在相同路徑與節點上定義。參數內容不會影響簽名比對(例如 func sendstuff():func sendstuff(arg1, arg2): 也算一致)。

若未符合上述條件(RPC 簽名不一致),腳本可能會出現錯誤或產生非預期行為。錯誤訊息有時甚至和你正在編寫的 RPC 無關。

詳細說明與疑難排解可參考這篇討論:<https://github.com/godotengine/godot/issues/57869#issuecomment-1034215138>。

@rpc 標註可帶入多個參數(皆有預設值)。@rpc 等同於:

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

各參數說明如下:

mode

  • "authority":只有 multiplayer authority 才能進行遠端呼叫。預設為伺服器,但可用 Node.set_multiplayer_authority 更改每個節點的權限。

  • "any_peer":允許任何 peer(包含用戶端)進行遠端呼叫,通常用來傳送玩家輸入等資料。

sync

  • "call_remote":此函式不會在本地 peer 上被呼叫。

  • "call_local":此函式也會在本地 peer 執行。適用於伺服器同時也是玩家的情境。

transfer_mode

  • "unreliable":資料封包不會被確認,可能遺失、亂序。

  • "unreliable_ordered":封包會按發送順序處理。如果較晚到達的封包已被後發的封包覆蓋,則會被直接忽略。若使用不當會造成資料遺失。

  • "reliable":會持續重送直到封包被正確收到,且封包順序固定,但效能影響較大。

transfer_channel 為通道索引。

前 3 個參數可任意順序,transfer_channel 必須最後寫。

你可以在 RPC 執行的函式內,透過 multiplayer.get_remote_sender_id() 取得發送端的唯一 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.

備註

RPC methods must be defined on Node-derived classes. Attempting to use high-level RPC calls on methods defined only in non-Node classes (such as Resource) will result in runtime errors.

通道(Channels)

現代網路協定支援多通道(channels),即在同一連線下建立多個獨立子連線,讓多條資料流互不干擾。

例如聊天室訊息與核心遊戲資料都要可靠傳送,但遊戲資料不應該被聊天訊息延遲影響。這時就應該用不同通道分開傳送。

通道也很適合搭配不可靠有序傳輸模式。此模式下若傳遞不同大小的封包,後到的封包可能會被直接丟棄,造成資料遺失。將不同功能資料分成多個相同型態封包的資料流,就能在有序傳輸下減少資料遺失,也不用承擔可靠模式帶來的延遲。

預設的 0 號通道(index 0)實際上有三個不同的通道——各自對應不同的傳輸模式。

大廳範例實作

這是一個能處理新玩家加入與離開、透過 signals 通知 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 = OfflineMultiplayerPeer.new()
    players.clear()


# 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():
    remove_multiplayer_peer()


func _on_server_disconnected():
    remove_multiplayer_peer()
    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)。詳情請見 匯出為專用伺服器

備註

本頁範例程式碼並未針對專用伺服器設計。你需要自行修改,讓伺服器不被視為玩家,也要調整遊戲啟動流程,讓第一個加入的玩家可以開始遊戲。

Authentication

Before hosting your game online to a public audience, you may want to consider adding authentication and protecting your RPCs against unauthenticated access. You can use the SceneMultiplayer's built-in authentication mechanism for this.

On the server:

# This goes after `multiplayer.multiplayer_peer = peer`.
multiplayer.auth_timout = 3
multiplayer.auth_callback = func(peer_id: int, payload: PackedByteArray):
    var auth_data: Dictionary = JSON.parse_string(payload.get_string_from_utf8())
    # Your authentication logic (such as checking the supplied username/password against a database)

    # Tell the MultiplayerAPI that the authentication was successful
    if authentication_successful:
        multiplayer.complete_auth(peer_id)

On the client:

# This goes after `multiplayer.multiplayer_peer = peer`.
multiplayer.auth_callback = func:
    # We have to set this on the client for the `peer_authenticating`
    # signal to emit.
    pass
multiplayer.peer_authenticating.connect(func(peer_id: int):
        var auth_data = {
            "username": "username",
            "password": "password",
        }
        multiplayer.send_auth(1, JSON.stringify(auth_data).to_utf8_buffer())

        # Tell the MultiplayerAPI that the authentication was successful.
        multiplayer.complete_auth(peer_id)

As soon as both the client's and the server's complete_auth() methods have been called, the connection is considered to be established and the connected_to_server and peer_connected signals fire.

Secure multiplayer design

Godot's high-level multiplayer API makes it easier to build networked games, but it does not automatically make gameplay logic secure. For competitive or persistent multiplayer games, treat all client input as untrusted.

A common mistake is to let clients authoritatively decide important game states, such as player position, combat results, inventory changes, or match outcomes. This can make cheating much easier, and result in more frequent desynchronization ("desync").

In general, prefer the following patterns:

  • Use server-authoritative logic for gameplay-critical decisions.

  • Validate RPC arguments before applying them to the game state.

  • Avoid trusting client-reported positions, timers, cooldowns, or resource values without checks.

  • Add safety checks and rate limits to actions that can be triggered frequently.

In short, you should design your networking so that the server remains the source of truth for important states.

For example, instead of accepting a client's final position directly, consider sending player input or movement intent to the authority/server, then validating and applying the result there. This comes with some tradeoffs (such as server-side performance and complexity due to the need for client-side prediction), but will make it much harder for attackers to cheat by sending falsified data.

See Choosing the right network model for your multiplayer game for more information on different multiplayer models and their security implications.