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 та деякі протоколи вищого рівня, такі як HTTP і SSL. Ці протоколи є гнучкими і можуть використовуватися майже для будь-чого. Однак використання їх для синхронізації стану гри вручну може скласти велику роботу. Іноді такої роботи неможливо уникнути або вона того варта, наприклад, під час роботи з власною реалізацією сервера на сервері. Але в більшості випадків варто розглянути мережевий API високого рівня Godot, який жертвує частиною тонкого контролю низькорівневої мережі заради більшої простоти використання.

Це пов'язано з обмеженнями притаманними протоколам низького рівня:

  • TCP гарантує, що пакети завжди надходитимуть надійно та в правильному порядку, але затримка, як правило, вища через виправлення помилок. Це також досить складний протокол, оскільки він розуміє, що таке "з'єднання", і оптимізується для цілей, які часто не підходять для таких застосунків, як багатокористувацькі ігри. Пакети буферизуються для надсилання більшими пачками, що призводить до менших затрат на пакет, але більшої затримки. Це може бути корисно для таких речей, як HTTP, але, зазвичай, не для ігор. Дещо з цього можна налаштувати та вимкнути (наприклад, відключивши «алгоритм Нейгла» для TCP-з'єднання).

  • UDP є простішим протоколом, який надсилає лише пакети (і не має поняття "з’єднання"). Відсутність виправлення помилок робить його досить швидким (затримка низька), але пакети можуть бути втрачені при передачі, або отримані в неправильному порядку. Крім того, MTU (максимальний розмір пакету) для UDP, як правило, малий (всього кілька сотень байтів), тому передача більших пакетів означає їх розділення, реорганізацію та повторну спробу, якщо частина дає збій.

Загалом, TCP можна вважати надійним, впорядкованим і повільним; UDP — ненадійним, неупорядкованим, але швидким. Через велику різницю в продуктивності часто є сенс перебудувати частини TCP, необхідні для ігор (додаткова надійність і порядок пакетів), уникаючи небажаних частин (функції контролю завантаженості/трафіку, алгоритм Нейгла тощо). Завдяки цьому більшість ігрових рушіїв мають таку реалізацію, і Godot не виняток.

Отже, ви можете використовувати API мережі низького рівня для максимального контролю та впроваджувати все поверх відкритих протоколів мережі, або використовувати API високого рівня на основі SceneTree, яке, загалом оптимізовано, виконує більшість важких завдань за лаштунками.

Примітка

Більшість платформ, які підтримує Godot, пропонують всі або більшість згаданих мережевих функцій високого та низького рівнів. Оскільки робота в мережі завжди значною мірою залежить від апаратного забезпечення та операційної системи, деякі функції можуть змінюватися або бути недоступними на деяких цільових платформах. Зокрема, платформа HTML5 наразі пропонує підтримку WebSockets і WebRTC, але не має деяких високорівневих функцій, а також необробленого доступу до низькорівневих протоколів, таких як TCP і UDP.

Примітка

Більше про TCP/IP, UDP та мережу: https://gafferongames.com/post/udp_vs_tcp/

У Gaffer On Games є багато корисних статей про мережу в іграх (тут), включаючи вичерпуюче введення в мережеві моделі в іграх.

Попередження

Додавання мережі до вашої гри вимагає певної відповідальності. Це може зробити вашу програму вразливою, якщо її зробити неправильно, і може призвести до чітів або експлойтів. Це навіть може дозволити зловмиснику скомпрометувати комп’ютери, на яких працює ваша програма, і використовувати ваші сервери для надсилання спаму, нападу на інших або викрадення даних ваших користувачів, якщо вони грають у вашу гру.

Таке завжди відбувається, коли задіяна мережа і це не має ніякого відношення до Godot. Ви, звичайно, можете експериментувати, але завжди, коли ви випускаєте мережеву програму, подбайте про всі можливі заходи безпеки.

Абстракція середнього рівня

Перш ніж перейти до того, як ми можемо синхронізувати гру в мережі, може бути корисно зрозуміти, як виконує синхронізацію базовий мережевий API.

Godot використовує об’єкт середнього рівня MultiplayerPeer. Цей об’єкт не призначений для безпосереднього створення, але розроблений таким чином, щоб кілька реалізацій C++ могли його надати.

Цей об’єкт поширюється від PacketPeer, тому він успадковує всі корисні методи для серіалізації, надсилання та отримання даних. Крім того, він додає методи встановлення однорангового вузла, режиму передачі тощо. Він також включає сигнали, які повідомлять вам, коли однорангові вузли підключаються, чи відключаються.

Цей інтерфейс класу може абстрагувати більшість типів мережевих рівнів, топологій та бібліотек. За замовчуванням Godot надає реалізацію на основі ENet (ENetMultiplayerPeer), одну на основі WebRTC (WebRTCMultiplayerPeer) та одну на основі WebSocket (WebSocketMultiplayerPeer), але це можна використовувати для реалізації мобільних API (для ad hoc WiFi, Bluetooth) або специфічних для пристроїв/консолей мережевих API.

У більшості поширених випадків використання цього об’єкта напряму не рекомендується, оскільки Godot надає мережеві можливості ще вищого рівня. Цей об’єкт все ще доступний, якщо гра потребує API нижчого рівня.

Розгляд хостингу

Під час розміщення сервера клієнти вашої LAN можуть підключатися за допомогою внутрішньої IP-адреси, яка зазвичай має форму 192.168.*.*. Ця внутрішня IP-адреса не доступна клієнтам, які не є локальною мережею/інтернетом.

У Windows ви можете знайти свою внутрішню IP-адресу, відкривши командний рядок і ввівши ipconfig. У macOS відкрийте термінал і введіть ifconfig. У Linux відкрийте термінал і введіть ip addr.

Якщо ви розміщуєте сервер на своїй власній машині та хочете, щоб до нього підключалися клієнти, що не належать до локальної мережі, вам, імовірно, доведеться перенаправити порт сервера на маршрутизаторі. Це потрібно, щоб зробити ваш сервер доступним з Інтернету, оскільки більшість домашніх з’єднань використовують NAT. Багатокористувацький API високого рівня Godot використовує лише UDP, тому ви повинні перенаправляти порт у UDP, а не лише в TCP.

Перенаправивши UDP-порт і переконавшись, що ваш сервер використовує цей порт, ви можете використовувати цей веб-сайт, щоб знайти свою публічну IP-адресу. Потім надайте цю загальнодоступну IP-адресу всім клієнтам Інтернету, які бажають підключитися до вашого сервера.

Високорівневий API Godot для багатокористувацької гри використовує модифіковану версію 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 = OfflineMultiplayerPeer.new()

Попередження

Під час експорту в Android обов’язково ввімкніть дозвіл INTERNET у попередньо налаштованих експортах Android, перш ніж експортувати проект, або використовувати розгортання одним клацанням мишки. Інакше Android заблокує будь-який мережевий зв’язок.

Управління з'єднаннями

Кожному вузлу присвоюється унікальний ідентифікатор. Ідентифікатор сервера завжди дорівнює 1, а клієнтам призначається випадкове позитивне ціле число.

Відповідь на підключення або відключення можлива шляхом підключення до сигналів MultiplayerAPI:

  • peer_connected(id: int) Цей сигнал випромінюється з ідентифікатором нещодавно підключеного вузла на кожному іншому вузлі та на новому вузлі кілька разів, один раз з ідентифікатором іншого вузла.

  • peer_disconnected(id: int) Цей сигнал надсилається кожному решті однорангового вузла, коли один з них від’єднується.

Решта випускаються тільки на клієнтах:

  • connected_to_server()

  • connection_failed()

  • server_disconnected()

Щоб отримати унікальний ідентифікатор пов’язаного вузла:

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, що означає, що вони повинні мати однакове ім’я. У разі використання add_child() для вузлів, які, як очікується, будуть використовувати RPC, встановіть аргумент force_readable_name на true.

Попередження

Якщо функція анотована @rpc у клієнтському сценарії (відповідно серверному сценарії), то ця функція також повинна бути оголошена в серверному сценарії (відповідно клієнтському сценарії). Обидва RPC повинні мати однаковий підпис, який оцінюється за допомогою контрольної суми усіх RPC. Усі RPC у сценарії перевіряються одночасно, і всі RPC мають бути оголошені як у сценаріях клієнта, так і в сценаріях сервера, навіть у функціях, які зараз не використовуються.

Сигнатура RPC включає декларацію @rpc(), функцію, тип повернення і NodePath. Якщо RPC міститься в сценарії, прикріпленому до /root/Main/Node1, тоді він повинен знаходитися в точно тому самому шляху та вузлі як у сценарії клієнта, так і в сценарії сервера. Аргументи функції не перевіряються на відповідність між серверним і клієнтським кодом (приклад: func sendstuff(): і func sendstuff(arg1, arg2): пропустять перевірку підпису).

Якщо ці умови не виконуються (якщо всі RPC не проходять перевірку підпису), скрипт може надрукувати помилку або викликати небажану поведінку. Повідомлення про помилку може бути не пов’язане з функцією RPC, яку ви зараз створюєте та тестуєте.

Дивіться додаткові пояснення та способи усунення несправностей у цій публікації.

Анотація може приймати ряд аргументів, які мають значення за замовчуванням. @rpc еквівалентний:

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

Параметри та їхні функції такі:

Режим:

  • "authority": лише повноваження для кількох гравців можуть дзвонити віддалено. За замовчуванням повноваженнями є сервер, але їх можна змінити для кожного вузла за допомогою Node.set_multiplayer_authority.

  • "any_peer": клієнтам дозволено дзвонити віддалено. Корисно для передачі даних користувача.

синхронізація:

  • "call_remote": функція не буде викликана на локальному вузлі.

  • "call_local": Функцію можна викликати на локальному одноранговому пристрої. Корисно, коли сервер також є гравцем.

режим_передачі:

  • "ненадійний" Пакети не підтверджуються, можуть бути втрачені та можуть надходити в будь-якому порядку.

  • "unreliable_ordered" Пакети надходять у тому порядку, в якому вони були надіслані. Це досягається шляхом ігнорування пакетів, які надходять пізніше, якщо інші, надіслані після них, уже отримані. При неправильному використанні може спричинити втрату пакетів.

  • "надійний" Повторні спроби надсилаються, доки пакети не будуть підтверджені, а їх порядок збережено. Має значне зниження продуктивності.

transfer_channel - це індекс каналу.

Перші 3 можна передавати в будь-якому порядку, але transfer_channel завжди має бути останнім.

Функцію multiplayer.get_remote_sender_id() можна використовувати для отримання унікального ідентифікатора відправника rpc, коли вона використовується у функції, що викликається 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.

Примітка

RPC methods must be defined on class_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 насправді складається з трьох різних каналів - по одному для кожного режиму передачі.

Приклад реалізації лобі

Це приклад лобі, який може керувати приєднанням і виходом однолітків, повідомляти сцени інтерфейсу користувача за допомогою сигналів і запускати гру після того, як усі клієнти завантажать сцену гри.

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.

Експорт для виділених серверів

Після створення багатокористувацької гри ви можете експортувати її для запуску на виділеному сервері без доступного графічного процесора. Дивіться Експорт для виділених серверів для додаткової інформації.

Примітка

Зразки коду на цій сторінці не призначені для роботи на виділеному сервері. Вам доведеться змінити їх, щоб сервер не вважався гравцем. Вам також доведеться змінити механізм початку гри, щоб перший гравець, який приєднається, міг почати гру.

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.