Представьте, что вы пишете свою десктопную мультиплеерную игру мечты, а потом вам захотелось, чтобы ваш проект также работал и в вебе. К сожалению, в интернете можно найти кучу гайдов по созданию только десктопного мультиплеера на Godot, но не браузерного мультиплеера, даже официальная документация самого движка никак не поможет и не предоставит простых примеров. У вас быстро опустятся руки и вы забьёте на эту затею, потому что ваш проект так и не заработает в браузере.
К счастью, эта проблема легко решается!
Вам может сказочно повезти, если поисковик выдаст этот пост на Reddit. Мне вот повезло, поэтому спешу рассказать и другим. Моя статья будет не столько переводом этого поста (и материалов, на которые она ссылается) на русский язык, сколько пересказом от моего лица с имеющимся опытом разработки + дополнительно поведаю о некоторых вещах, что не были упомянуты в посте.
Разделяем свой проект на серверную и клиентскую части
Игровой движок Godot предоставляет разработчикам на выбор два варианта многоранговой сети:
Listen-сервер (или хост-клиент) - один из игроков является также сервером, а остальные игроки подключаются к нему. Если игрок-сервер закроет игру, также будут выкинуты из сессии все остальные игроки. В таком случае у нас на руках будет один проект.
Клиент-сервер - сервер не является игроком и нужен только для хранения и передачи информации для всех подключенных и подключающихся игроков. В таком случае у нас на руках находится уже два проекта - это, собственно, серверная часть и клиентская часть.
Нам нужна клиент-серверная архитектура. Как она выглядит в Godot вы можете посмотреть в этом видео.
Немного пояснений
По-умолчанию мультиплеер на Godot работает с протоколом UDP, однако браузеры просто так не примут этот протокол. Чтобы мультиплеер заработал после экспорта в HTML5, нам нужны либо веб-сокеты, которые работают поверх протокола TCP, либо WebRTC.
Если вы загляните в документацию Godot, то увидите, что веб-сокеты и WebRTC реализуются сложно и непонятно. Но это всё не нужно, потому что в Godot есть одна хитрость, благодаря которой можно и веб-сокеты использовать, и код оставить прежним. Как говорится, и рыбку съесть, и костью не подавиться.
В Godot есть два класса WebSocketServer и WebSocketClient. В документации говорится, что после экспорта проекта в HTML5 WebSocketServer перестанет работать из-за ограничений браузеров. Однако это не говорится в случае с WebSocketClient. Это означает, что клиентскую часть мы можем экспортировать в веб, а серверную часть придётся экспортировать как десктопное приложение, которое, например, мы можем запустить на сервере. Именно поэтому мы и выбрали архитектуру "клиент-сервер", потому что только она и будет работать.
Пишем код
Как правило в серверной части пишут следующий код:
var port = 8080
var max_clients = 20
func _ready():
get_tree().connect("network_peer_connected", self, "client_connected")
get_tree().connect("network_peer_disconnected", self, "client_disconnected")
var server = NetworkedMultiplayerENet.new()
server.create_server(port, max_clients)
get_tree().set_network_peer(server)
а в клиентской части этот код:
# Локальный IP для запуска и проверки работоспособности мультиплеера на своей машине.
var ip = '127.0.0.1'
var port = 8080
func _ready():
get_tree().connect("connected_to_server", self, "connected_to_server")
get_tree().connect("server_disconnected", self, "server_disconnected")
var client = NetworkedMultiplayerENet.new()
client.create_client(ip, port)
get_tree().set_network_peer(client)
В случае с веб-сокетом достаточно заменить несколько строк кода в серверной части:
var server
var port = 8080
func _ready():
get_tree().connect("network_peer_connected", self, "client_connected")
get_tree().connect("network_peer_disconnected", self, "client_disconnected")
server = WebSocketServer.new()
server.listen(port, PoolStringArray(), true)
get_tree().set_network_peer(server)
func _process(delta):
if server.is_listening():
server.poll()
и клиентской части:
var client
var ip = 'ws://127.0.0.1:'
var port = '8080'
func _ready():
get_tree().connect("connected_to_server", self, "connected_to_server")
get_tree().connect("server_disconnected", self, "server_disconnected")
client = WebSocketClient.new()
var url = ip + port
var error = client.connect_to_url(url, PoolStringArray(), true)
get_tree().set_network_peer(client)
func _process(delta):
if client.get_connection_status() in [NetworkedMultiplayerPeer.CONNECTION_CONNECTED, NetworkedMultiplayerPeer.CONNECTION_CONNECTING]:
client.poll()
Скорее всего, у вас не будет проблем с передачей данных между сервером и клиентом. У меня же проект был специфический и при запуске клиент принимал от сервера большой массив данных. Если вам тоже по какой-то причине понадобилось гонять огромные данные, то вам нужно зайти в настройки проекта -> Основное -> Network -> Limits -> Websocket Server (и Websocket Client) и увеличить значения полей Max Out Buffer Kb и Max In Buffer Kb.
Сертификат и HTTPS
Чтобы мультиплеер заработал на сайте с соединением через HTTPS (ещё один безопасный протокол), нужно обязательно получить криптографический сертификат и ключ. Но сертификат не должен быть самоподписанным, например, полученным при помощи утилиты OpenSSL. Сертификат должен быть получен из официальных центров, например, центра "Let's Encrypt".
Для этого нужно получить или купить доменное имя. Представим, что купили домен domainnameserver.ru
Там, где получали или покупали домен, должна быть возможность настроить DNS. В этих настройках нужно добавить поддомен _acme-challenge, в результате в наличие будет домен domainnameserver.ru и поддомен _acme-challenge.domainnameserver.ru
Теперь нужно арендовать VPS (Virtual Private Server) желательно на дистрибутиве Ubuntu или Debian. Заходим в терминал сервера, логин будет root, а пароль тот, что указывали при оформлении аренды.
Обновляемся:
apt update
apt upgrade
Устанавливаем утилиту Certbot:
sudo apt-get install certbot
Отправляем запрос в центр сертификации. После команды -d вписываем свой домен.
certbot certonly –-manual -d domainnameserver.ru –-agree-tos –-manual-public-ip-logging-ok –-preferred-challenges dns-01 –-server https://acme-v02.api.letsencrypt.org/directory –-register-unsafely-without-email –-rsa-key-size 4096
Certbot вернёт запись типа TXT, после чего встанет на паузу:
Эту запись нужно скопировать, после чего вернуться к настройкам DNS и выбрать поддомен _acme-challenge.domainnameserver.ru.
У каждого домена и поддомена имеются записи различных типов (A, MX, TXT и т.д). Нам нужен тип TXT, поэтому заменяем у поддомена запись TXT на скопированную запись от Certbot.
И пока мы находимся в настройках DNS, можно ещё в запись типа A у домена domainnameserver.ru вписать внешний IP виртуального сервера.
Возвращаемся в терминал виртуального сервера и нажимаем Enter, чтобы Certbot продолжил работу. Сертификат и ключ появятся в директории /etc/letsencrypt/live/domainnameserver.ru
Переходим в эту директорию. Не обращаем внимания на файлы cert.pem и chain.pem, важны только файлы fullchain.pem и key.pem. Эти два файла при помощи утилиты OpenSSL конвертируем в понятные для Godot форматы CRT и KEY.
cd /etc/letsencrypt/live/domainnameserver.ru
openssl x509 -outform der -in fullchain.pem -out fullchain.crt
openssl rsa -outform PEM -in privkey.pem -out privkey.key
Полученные файлы fullchain.crt и privkey.key скачиваем на свой компьютер через FTP-клиент и помещаем эти файлы в папку с серверной частью нашего мультиплеера.
В методе _ready() загрузим сертификат и ключ, т.е. добавим всего пару строк кода. Теперь код должен выглядеть так:
var server
var port = 8080
func _ready():
get_tree().connect("network_peer_connected", self, "client_connected")
get_tree().connect("network_peer_disconnected", self, "client_disconnected")
server = WebSocketServer.new()
server.private_key = load("res://HTTPSKeys/privkey.key")
server.ssl_certificate = load("res://HTTPSKeys/fullchain.crt")
server.listen(port, PoolStringArray(), true)
get_tree().set_network_peer(server)
func _process(delta):
if server.is_listening():
server.poll()
Помимо этого заходим в настройки проекта -> Основное -> Network -> Ssl и в поле Certificates указываем файл fullchain.crt
В клиентской части заменяем var ip = 'ws://127.0.0.1:' на var ip = 'wss://domainnameserver.ru:'. Убедитесь, что у домена в записи A указан IP-адрес сервера.
Запуск серверной части
Экспортируем нашу серверную часть под Linux в формате PCK. Давайте обзовём его server.pck
Желательно создать папку на сервере (команда mkdir). Обзовём её "game" и перекинем туда файл server.pck (опять же через FTP-клиент). Перемещаемся в саму папку game при помощи команды cd.
cd
mkdir /game
cd /game
ИЛИ
Вместо FTP-клиента вы можете загрузить свой файл на облако (например, file.io), после чего скачать на свой сервер через команду wget. Но имя вашего файла изменится, не забудьте переименовать его обратно в server.pck для удобства (команда mv).
Если вы не уверены в том как называется скачанный файл, можете воспользоваться командой ls, он отобразит названия всех файлов в папке. Если вы сделали какую-то ошибку и нужно удалить файл или папку, посмотрите команду rm.
cd /game
wget ссылка_на_файл_в_облаке
mv старое_название_файла server.pck
Находясь в папке game, скачиваем Godot-сервер с официального сайта Godot, распаковываем и через него запускаем server.pck
wget https://downloads.tuxfamily.org/godotengine/3.4.4/Godot_v3.4.4-stable_linux_server.64.zip
unzip Godot_v3.4.4-stable_linux_server.64.zip
./Godot_v3.4.4-stable_linux_server.64 --main-pack server.pck
Если вы пользуетесь другой версией Godot, не забудьте поменять цифры. Возможно, данная статья не актуальна для нового Godot 4.0
Можете для разнообразия глянуть это видео, но не обязательно.
Запуск клиентской части
Всё готово! Осталось только экспортировать клиентскую часть в HTML5. Не забудьте проверить, что файл для запуска называется index.html
Теперь вы можете выкладывать клиентскую часть на свой хост или на сайты, которые предназначены для игр, например, itch.io или gotm.io. Кстати, gotm.io как раз предназначен для запуска игр, разработанных на Godot.
Если вы следовали инструкции, то у вас всё заработает. Вы превосходны!