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

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

Godot는 항상 UDP, TCP, 그 외 SSL이나 HTTP와 같은 일부 고수준 프로토콜을 통해, 일반적인 저수준 네트워킹을 지원했습니다. 이 프로토콜은 유연하고 거의 모든 것에 사용될 수 있습니다. 하지만 게임 상태를 일일이 동기화하기 위해 이 프로토콜을 사용하는 일은 많은 작업이 필요합니다. 때로는 그 작업이 필요하고, 가치가 있습니다. 예를 들면 백엔드에서 커스텀 서버 구현을 작업할 때가 있죠. 그러나 대부분의 경우, 저수준 네트워킹의 세부적인 제어를 희생하고 사용 편의성을 높이는 Godot의 고수준 네트워킹 API를 고려해 보는 게 좋습니다.

이는 저수준 프로토콜의 고유의 한계 때문입니다:

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

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

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

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

참고

Godot의 지원하는 플랫폼 대부분은 앞서 언급한 고수준과 저수준 네트워킹 기능을 거의 전부 제공합니다. 그러나 네트워킹은 항상 하드웨어와 운영 체제에 따라 크게 달라지므로, 일부 기능은 특정 플랫폼에서 달라지거나 이용할 수 없을지도 모릅니다. 가장 주목할 것으로, HTML5 플랫폼은 현재로써 오직 WebSocket 지원을 제공하고 고수준 기능은 부족하고, TCP 및 UDP와 같은 저수준 프로토콜로 원시 접근만 지원합니다.

참고

TCP/IP, UDP, 네트워킹에 더 알아보세요: https://gafferongames.com/post/udp_vs_tcp/

Gaffer On Games에는 게임의 네트워크에 관한 많은 유용한 기사가 있습니다 (여기로). 기사 중에는 포괄적인 게임에서 네트워킹 모델 소개도 있습니다.

경고

게임에 네트워킹을 추가하는 일은 책임도 따릅니다. 이 작업은 애플리케이션이 잘못되어 사기나 착취에 취약해질 수 있습니다. 게다가 공격자가 실행 중인 애플리케이션의 시스템을 손상시키고, 서버를 통해 스팸을 보내거나, 다른 이를 공격하고, 게임을 하고 있던 사용자 정보를 훔칠 수도 있습니다.

이 일은 네트워킹에 관련되어 있고 Godot와는 관련이 없는 경우입니다. 물론 시험을 해볼 수는 있지만, 네트워크가 연결된 애플리케이션을 출시하면, 가능한 보안 문제를 항상 관리하세요.

중급 추상화(Mid level abstraction)

어떻게 네트워크를 통해 게임을 동기화할 지 알아보기 전에, 기본 네트워크 API가 어떻게 동기화에 작동하는지 이해하는 것이 좋습니다.

Godot는 중급(mid level) 오브젝트로 NetworkedMultiplayerPeer를 사용합니다. 이 오브젝트는 바로 네트워크를 만드는 것은 아니지만, 설계를 함으로써 여러 C++ 구현 기능을 제공합니다.

이 오브젝트는 PacketPeer에서 확장됩니다. 따라서 직렬화(Serialize), 데이터 보내기 및 받기에 유용한 메서드를 갖습니다. 또한 피어(Peer), 전송 모드(Transfer Mode) 등을 설정하는 메서드를 추가합니다. 그리고 시그널을 갖고 있어 언제 피어가 연결되고 끊기는지 알 수 있습니다.

이 클래스 인터페이스는 대부분의 네트워크 계층, 토폴로지 및 라이브러리 유형을 추상화할 수 있습니다. 기본적으로 Godot는 ENet(ENetMultiplayerPeer) 기반 구현, WebRTC(WebRTCMultiplayerPeer) 기반 구현, 그리고 WebSocket 기반 구현을 제공합니다. (WebSocketMultiplayerPeer), 그러나 이는 모바일 API(Ad Hoc WiFi, Bluetooth용) 또는 사용자 정의 장치/콘솔별 네트워킹 API를 구현하는 데 사용될 수 있습니다.

대부분의 일반적인 경우, Godot는 더 높은 수준의 네트워킹 기능을 제공하므로 이 객체를 직접 사용하는 것은 권장되지 않습니다. 이 개체는 게임에 하위 수준 API에 대한 특정 요구 사항이 있는 경우 계속 사용할 수 있습니다.

내보내기 고려 사항

서버를 호스팅할 때 LAN(근거리 통신망)`의 클라이언트는 일반적으로 ``192.168.*.*` 형식의 내부 IP 주소를 사용하여 연결할 수 있습니다. 이 내부 IP 주소는 LAN/인터넷이 아닌 클라이언트에서는 연결할 수 없습니다.

Windows에서는 명령 프롬프트를 열고 ``ipconfig``를 입력하여 내부 IP 주소를 찾을 수 있습니다. macOS에서 터미널을 열고 ``ifconfig``를 입력합니다. Linux에서는 터미널을 열고 ``ip addr``를 입력합니다.

자신의 컴퓨터에서 서버를 호스팅하고 LAN이 아닌 클라이언트가 해당 서버에 연결하도록 하려면 라우터에서 서버 포트를 *전달*해야 할 것입니다. 대부분의 주거용 연결은 `NAT <https://en.wikipedia.org/wiki/Network_address_translation>`__을 사용하므로 인터넷에서 서버에 연결할 수 있도록 하려면 이 작업이 필요합니다. Godot의 고급 멀티플레이어 API는 UDP만 사용하므로 TCP뿐만 아니라 UDP로 포트를 전달해야 합니다.

UDP 포트를 전달하고 서버가 해당 포트를 사용하는지 확인한 후 `이 웹사이트 <https://icanhazip.com/>`__을 사용하여 공용 IP 주소를 찾을 수 있습니다. 그런 다음 이 공용 IP 주소를 서버에 연결하려는 모든 인터넷 클라이언트에 제공하십시오.

Godot의 고급 멀티플레이어 API는 완전한 IPv6 지원을 허용하는 수정된 버전의 ENet을 사용합니다.

네트워크 초기화 중

Godot의 고급 네트워킹은 :ref:`SceneTree <class_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 = OfflineMultiplayerPeer.new()

경고

Android로 내보낼 때 프로젝트를 내보내거나 원클릭 배포를 사용하기 전에 Android 내보내기 사전 설정에서 INTERNET 권한을 활성화해야 합니다. 그렇지 않으면 모든 종류의 네트워크 통신이 Android에 의해 차단됩니다.

연결 관리

모든 피어에는 고유한 ID가 할당됩니다. 서버의 ID는 항상 1이고 클라이언트에는 임의의 양의 정수가 할당됩니다.

``MultiplayerAPI``의 시그널에 연결하면 연결 또는 연결 끊김에 대한 응답이 가능합니다.

  • peer_connected(id: int) 이 시그널는 서로 다른 피어에서 새로 연결된 피어의 ID와 함께 방출되고, 새 피어에서는 서로의 피어 ID와 함께 한 번 여러 번 방출됩니다.

  • peer_disconnected(id: int) 이 시그널는 하나의 연결이 끊어지면 나머지 모든 피어에서 방출됩니다.

다음과 같은 주의 사항이 있습니다:

  • 연결(Connections)

  • 연결(Connections)

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

RPCs will not serialize Objects or Callables.

원격 호출이 성공하려면 송수신 노드의 NodePath``가 동일해야 합니다. 즉, 이름이 동일해야 합니다. RPC를 사용할 것으로 예상되는 노드에 대해 ``add_child()``를 사용하는 경우 ``force_readable_name 인수를 ``true``로 설정합니다.

경고

클라이언트 스크립트(각각 서버 스크립트)에서 함수에 @rpc 주석이 달린 경우 이 함수는 서버 스크립트(각각 클라이언트 스크립트)에서도 선언되어야 합니다. 두 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("authority", "call_remote", "reliable", 0)

가장 일반적인 사용 케이스는 다음과 같습니다:

mode:

  • "authority": 멀티플레이어 권한만 원격으로 호출할 수 있습니다. 권한은 기본적으로 서버이지만 :ref:`노드.set_multiplayer_authority <class_Node_method_set_multiplayer_authority>`를 사용하여 노드별로 변경할 수 있습니다.

  • "any_peer": 클라이언트가 원격으로 호출할 수 있습니다. 사용자 입력을 전송하는 데 유용합니다.

sync:

  • "call_remote": 이 함수는 로컬 피어에서 호출되지 않습니다.

  • "call_local": 이 함수는 로컬 피어에서 호출될 수 있습니다. 서버가 플레이어일 때 유용합니다.

transfer_mode:

  • "unreliable" 패킷은 승인되지 않고 손실될 수 있으며 어떤 순서로든 도착할 수 있습니다.

  • "unreliable_ordered" 패킷은 전송된 순서대로 수신됩니다. 이는 나중에 전송된 다른 패킷이 이미 수신된 경우 나중에 도착하는 패킷을 무시함으로써 달성됩니다. 잘못 사용하면 패킷 손실이 발생할 수 있습니다.

  • "reliable" 재전송 시도는 패킷이 승인될 때까지 전송되며 해당 순서가 유지됩니다. 상당한 성능 저하가 있습니다.

``transfer_channel``는 채널 인덱스입니다.

처음 3개는 순서에 관계없이 전달될 수 있지만 ``transfer_channel``는 항상 마지막이어야 합니다.

multiplayer.get_remote_sender_id() 함수는 rpc가 호출하는 함수 내에서 사용될 때 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.

참고

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.

채널

최신 네트워킹 프로토콜은 연결 내에서 별도의 연결인 채널을 지원합니다. 이를 통해 서로 간섭하지 않는 여러 패킷 스트림이 가능해집니다.

예를 들어 게임 채팅 관련 메시지와 일부 핵심 게임플레이 메시지는 모두 안정적으로 전송되어야 하지만 게임플레이 메시지는 채팅 메시지가 승인될 때까지 기다리면 안 됩니다. 이는 다양한 채널을 사용하여 달성할 수 있습니다.

채널은 신뢰할 수 없는 순서 전송 모드와 함께 사용될 때도 유용합니다. 이 전송 모드를 사용하여 가변 크기의 패킷을 전송하면 도착 속도가 느린 패킷이 무시되므로 패킷 손실이 발생할 수 있습니다. 채널을 사용하여 이를 동종 패킷의 여러 스트림으로 분리하면 패킷 손실이 거의 없고 신뢰성 모드로 인해 발생하는 대기 시간 패널티 없이 순서대로 전송할 수 있습니다.

인덱스 0의 기본 채널은 실제로 세 개의 서로 다른 채널(각 전송 모드당 하나씩)입니다.

구현

피어의 가입 및 탈퇴를 처리하고, 시그널를 통해 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를 사용할 수 없는 전용 서버에서 실행하기 위해 내보내고 싶을 수도 있습니다. 자세한 내용은 :ref:`doc_exporting_for_dedicated_servers`를 참조하세요.

참고

이 페이지의 코드 샘플은 전용 서버에서 실행되도록 설계되지 않았습니다. 서버가 플레이어로 간주되지 않도록 수정해야 합니다. 또한 처음으로 참가하는 플레이어가 게임을 시작할 수 있도록 게임 시작 메커니즘을 수정해야 합니다.

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.