Up to date

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

High-level multiplayer

High-level vs low-level API

여기부터는 몇 가지 기본 사항과 함께, Godot에서 하이 레벨 네트워킹과 로우 레벨 네트워킹의 차이를 설명합니다. 바로 실전으로 가서 첫 노드에 네트워킹을 추가하고 싶다면, 아래의 네트워크 초기화하기(Initializing the network)로 건너 뛰세요. 하지만 나중에라도 이 부분을 읽어주세요!

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는 패킷(Packet)이 항상 안전하게 도착하도록 보장합니다. 하지만 오류 연결 때문에 지연 시간은 점진적으로 길어집니다. 이 또한 복잡한 프로토콜입니다. 왜냐하면 무엇이 "연결(Connection)"인지를 이해해야 하고, 멀티플레이어 게임과 같은 애플리케이션과는 맞지 않은 목적을 이루기 위해 최적화를 해야 합니다. 패킷은 더 큰 배치(Batch)로 전송되도록 버퍼링됩니다. 그렇게 되면 전달하는 패킷 당 오버헤드(Overhead)는 줄어들고 지연 시간이 길어집니다. 이는 HTTP에는 유용하겠지만, 일반적인 게임에는 아니죠. 일부 프로토콜은 이를 설정하거나 끌 수 있습니다. (예: TCP 연결의 "네이글 알고리즘(Nagle Algorithm)"을 끔).

  • UDP는 더 간단한 프로토콜로, 패킷을 보내기만 합니다 (즉, "연결(Connection)"의 개념이 없습니다). 오류 연결이 없어서 꽤 빠릅니다 (짧은 지연 시간). 하지만 패킷을 보내는 과정에서 잃을 수 있고, 잘못된 상대방이 받을 수 있습니다. 게다가, UDP의 MTU (최대 패킷 크기)는 (겨우 몇 백 바이트로) 보통 낮습니다. 따라서 더 큰 패킷을 전송하려면 패킷을 분리하고, 다시 구조화하고, 만일 일부분이 잘못되면 다시 시도해야 합니다.

보통은, TCP를 신뢰할 수 있고 질서 있고 느리다고 생각할 수 있습니다. 반대로 UDP는 신뢰할 수 없고, 무질서하며, 빠르다고 생각하겠죠. 그 이유는 둘 간의 큰 성능 차이입니다. 종종 게임에 필요한 TCP 부분을 새로 만드는 것이 합리적이기도 합니다 (선택적인 안정성과 패킷 순서). 그러면서 원하지 않은 부분은 피할 수 있으니까요 (혼잡(Congestion)/트래픽(traffic) 제어 기능, 네이글 알고리즘 등). 이 때문에 대부분의 게임 엔진은 이러한 네트워킹 구현을 제공합니다. Godot 역시 예외가 아니죠.

요약해서 말하자면, 최대한의 제어와 순수한 네트워크 프로토콜에서 모든 것을 구현하려면, 로우 레벨 네트워킹 API를 사용할 수 있습니다. 혹은 일반적으로 최적화 된 방식에서 씬 뒤로 대부분의 무거운 리프팅을 수행하는 SceneTree(씬 트리)에서 하이 레벨 API를 사용할 수 있습니다.

참고

Most of Godot's supported platforms offer all or most of the mentioned high- and low-level networking features. As networking is always largely hardware and operating system dependent, however, some features may change or not be available on some target platforms. Most notably, the HTML5 platform currently offers WebSockets and WebRTC support but lacks some of the higher-level features, as well as raw access to low-level protocols like TCP and 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

경고

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와는 관련이 없는 경우입니다. 물론 시험을 해볼 수는 있지만, 네트워크가 연결된 애플리케이션을 출시하면, 가능한 보안 문제를 항상 관리하세요.

Mid-level abstraction

어떻게 네트워크를 통해 게임을 동기화할 지 알아보기 전에, 기본 네트워크 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에서 확장됩니다. 따라서 직렬화(Serialize), 데이터 보내기 및 받기에 유용한 메서드를 갖습니다. 또한 피어(Peer), 전송 모드(Transfer Mode) 등을 설정하는 메서드를 추가합니다. 그리고 시그널을 갖고 있어 언제 피어가 연결되고 끊기는지 알 수 있습니다.

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.

Hosting considerations

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.

Initializing the network

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

경고

When exporting to Android, make sure to enable the INTERNET permission in the Android export preset before exporting the project or using one-click deploy. Otherwise, network communication of any kind will be blocked by Android.

Managing connections

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

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.

sync:

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

transfer_mode:

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

Channels

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.

Example lobby implementation

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.

데디케이티드 서버로 내보내기

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 데디케이티드 서버로 내보내기 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.