Сетевое взаимодействие 26.1.2
Общее руководство по работе с сетью с использованием Fabric API.
Сетевое взаимодействие в Minecraft используется для связи между клиентом и сервером. Это обширная тема, поэтому данная страница разделена на несколько категорий.
Почему сетевое взаимодействие важно?
Пакеты — это ключевое понятие сетевого взаимодействия в Minecraft. Пакеты состоят из произвольных данных, которые могут передаваться как от сервера к клиенту, так и от клиента к серверу. Ознакомьтесь со схемой ниже, которая наглядно иллюстрирует архитектуру сети в Fabric:

Обратите внимание, что пакеты служат мостом между сервером и клиентом. Это связано с тем, что почти любое ваше действие в игре так или иначе задействует сеть, знаете вы об этом или нет. Например, когда вы отправляете сообщение в чат, на сервер уходит пакет с его содержимым. Затем сервер отправляет другой пакет со средством связи всем остальным клиентам.
Важно помнить, что сервер работает всегда — даже в одиночной игре и в мире для локальной сети (LAN). Пакеты всё равно используются для связи между клиентом и сервером, даже если с вами никто больше не играет. Когда речь заходит о сторонах в контексте сети, используются термины "логический клиент" (logical client) и "логический сервер" (logical server). Встроенный сервер одиночной/локальной игры и выделенный сервер являются логическими серверами, но только выделенный сервер можно считать физическим сервером.
Когда состояние между клиентом и сервером не синхронизировано, могут возникать проблемы, при которых сервер или другие клиенты «не согласны» с действиями конкретного клиента. Это явление часто называют «рассинхронизацией» (desync). При написании собственного мода вам может потребоваться отправить пакет данных, чтобы синхронизировать состояние сервера и всех клиентов.
Введение в сетевое взаимодействие
Определение полезной нагрузки (Payload)
INFO
Полезная нагрузка (payload) — это данные, которые переносятся внутри пакета.
Это можно сделать путем создания Java-записи (Record) с параметром BlockPos, реализующей интерфейс CustomPacketPayload.
java
No lines matched.1
В то же время мы определили:
- Идентификатор (
Identifier), используемый для распознавания полезной нагрузки нашего пакета. В данном примере наш идентификатор будетexample-mod:summon_lightning.
java
// #region identifier1
- Открытый статический экземпляр (public static)
CustomPayload.Typeдля уникальной идентификации этой пользовательской полезной нагрузки. Мы будем ссылаться на этот ID как в общем коде (common), так и в клиентском (client).
java
public static final Identifier SUMMON_LIGHTNING_PAYLOAD_ID = Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "summon_lightning");1
- Открытый статический экземпляр (public static)
StreamCodec, чтобы игра знала, как сериализовать/десериализовать содержимое пакета.
java
// #endregion identifier1
Мы также переопределили метод type, чтобы он возвращал ID нашей полезной нагрузки.
java
// #region payload_type
public static final CustomPacketPayload.Type<ClientboundSummonLightningPayload> TYPE = new CustomPacketPayload.Type<>(SUMMON_LIGHTNING_PAYLOAD_ID);
// #endregion payload_type1
2
3
4
2
3
4
Регистрация полезной нагрузки
Прежде чем отправить пакет с нашей полезной нагрузкой, нам необходимо зарегистрировать её на обеих физических сторонах.
Это можно сделать в нашем общем инициализаторе (common initializer) с помощью метода PayloadTypeRegistry.clientboundPlay().register, который принимает CustomPayload.Type и StreamCodec.
java
// #region clientbound1
Аналогичный метод существует для регистрации полезной нагрузки, отправляемой от клиента к серверу: PayloadTypeRegistry.serverboundPlay().register.
Отправка пакета клиенту
Чтобы отправить пакет с нашей полезной нагрузкой, мы можем использовать метод ServerPlayNetworking.send, который принимает ServerPlayer и CustomPayload.
Давайте начнем с создания нашего предмета «Молниеносная картошка» (Lightning Tater). Вы можете переопределить метод use, чтобы запускать действие при использовании предмета. В данном случае давайте отправим пакеты игрокам на данном уровне сервера (server level).
java
No lines matched.1
Давайте рассмотрим приведенный выше код.
Мы отправляем пакеты только тогда, когда действие инициировано на сервере, выполняя ранний возврат (return) с помощью проверки isClientSide():
java
// #region client_check
if (level.isClientSide()) {
return InteractionResult.PASS;1
2
3
2
3
Мы создаем экземпляр полезной нагрузки с позицией пользователя:
java
// #endregion client_check1
Наконец, мы получаем список игроков на уровне сервера через PlayerLookup и отправляем пакет каждому игроку.
java
// #region payload_instance
ClientboundSummonLightningPayload payload = new ClientboundSummonLightningPayload(user.blockPosition());
// #endregion payload_instance1
2
3
2
3
INFO
Fabric API предоставляет PlayerLookup — набор вспомогательных функций для поиска игроков на сервере.
Термин, часто используемый для описания функционала этих методов — «трекинг» (tracking, отслеживание). Это означает, что объект или чанк на сервере известен клиенту игрока (на расстоянии его обзора), и сущность или блок должны уведомлять отслеживающих клиентов об изменениях.
Трекинг — важная концепция для оптимизации сетевого взаимодействия. Благодаря ему о произошедших изменениях путем отправки пакетов уведомляются только те игроки, которым это необходимо.
Получение пакета на стороне клиента
Чтобы получить пакет, отправленный с сервера на клиент, вам необходимо указать, как именно вы будете обрабатывать входящий пакет.
Это можно сделать в инициализаторе клиента (client initializer), вызвав метод ClientPlayNetworking.registerGlobalReceiver и передав туда CustomPayload.Type и PlayPayloadHandler, который является функциональным интерфейсом.
В данном случае мы определим запускаемое действие внутри реализации PlayPayloadHandler (в виде лямбда-выражения).
java
No lines matched.1
Давайте рассмотрим приведенный выше код.
Мы можем получить доступ к данным из нашей полезной нагрузки (payload), вызвав геттеры записи (Record). В данном случае это payload.pos(), который затем можно использовать для получения координат X, Y и Z.
java
// #region payload_pos1
Наконец, мы создаем сущность LightningBolt (молнию) и добавляем ее в мир (level).
java
BlockPos lightningPos = payload.pos();
// #endregion payload_pos
// #region lightning_bolt
LightningBolt entity = EntityType.LIGHTNING_BOLT.create(level, EntitySpawnReason.TRIGGERED);1
2
3
4
5
6
2
3
4
5
6
Теперь, если вы добавите этот мод на сервер, то при использовании игроком предмета Lightning Tater все игроки увидят удар молнии в позиции этого пользователя.
Отправка пакета на сервер
Как и в случае с отправкой пакета на клиент, мы начинаем с создания пользовательской полезной нагрузки (custom payload). На этот раз, когда игрок использует "Ядовитый картофель" на живой сущности, мы запрашиваем у сервера наложение на нее эффекта «Свечение» (Glowing).
java
No lines matched.1
Мы передаем соответствующий кодек (codec) вместе со ссылкой на метод (method reference) для получения значения из Record для сборки этого кодека.
Затем мы регистрируем нашу полезную нагрузку в общем инициализаторе. Однако на этот раз как пакет от клиента к серверу (Client-to-Server payload), используя метод PayloadTypeRegistry.serverboundPlay().register.
java
PayloadTypeRegistry.clientboundPlay().register(ClientboundSummonLightningPayload.TYPE, ClientboundSummonLightningPayload.CODEC);1
Чтобы отправить пакет, давайте добавим действие при использовании игроком "Ядовитого картофеля". Мы будем использовать событие UseEntityCallback, чтобы код оставался лаконичным.
Мы регистрируем это событие в нашем инициализаторе клиента и используем isClientSide(), чтобы убедиться, что действие будет запущено только на логическом клиенте.
java
No lines matched.1
Мы создаем экземпляр нашего класса GiveGlowingEffectServerboundPayload с необходимыми аргументами. В данном случае это сетевой ID (network ID) целевой сущности.
java
}1
Наконец, мы отправляем пакет на сервер, вызывая метод ClientPlayNetworking.send с экземпляром нашей полезной нагрузки GiveGlowingEffectServerboundPayload.
java
1
Получение пакета на сервере
Это можно сделать в обычном инициализаторе, вызвав ServerPlayNetworking.registerGlobalReceiver и передав CustomPayload.Type и PlayPayloadHandler.
java
No lines matched.1
INFO
Очень важно проверять содержимое пакета на стороне сервера.
В данном случае мы проверяем, существует ли сущность, на основе ее сетевого ID.
java
// #endregion serverbound1
Кроме того, целевая сущность должна быть живой сущностью (living entity), и мы ограничиваем расстояние от игрока до целевой сущности пятью блоками.
java
ServerPlayNetworking.registerGlobalReceiver(GiveGlowingEffectServerboundPayload.TYPE, (payload, context) -> {1
Теперь, когда любой игрок попытается использовать "Ядовитый картофель" на живой сущности, к ней применится эффект свечения.

















