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

Обратите внимание, что пакеты служат мостом между сервером и клиентом. Это связано с тем, что почти любое ваше действие в игре так или иначе задействует сеть, знаете вы об этом или нет. Например, когда вы отправляете сообщение в чат, на сервер уходит пакет с его содержимым. Затем сервер отправляет другой пакет со средством связи всем остальным клиентам.
Важно помнить, что сервер работает всегда — даже в одиночной игре и в мире для локальной сети (LAN). Пакеты всё равно используются для связи между клиентом и сервером, даже если с вами никто больше не играет. Когда речь заходит о сторонах в контексте сети, используются термины "логический клиент" (logical client) и "логический сервер" (logical server). Встроенный сервер одиночной/локальной игры и выделенный сервер являются логическими серверами, но только выделенный сервер можно считать физическим сервером.
Когда состояние между клиентом и сервером не синхронизировано, могут возникать проблемы, при которых сервер или другие клиенты «не согласны» с действиями конкретного клиента. Это явление часто называют «рассинхронизацией» (desync). При написании собственного мода вам может потребоваться отправить пакет данных, чтобы синхронизировать состояние сервера и всех клиентов.
Введение в сетевое взаимодействие
Определение полезной нагрузки (Payload)
INFO
Полезная нагрузка (payload) — это данные, которые переносятся внутри пакета.
Это можно сделать путем создания Java-записи (Record) с параметром BlockPos, реализующей интерфейс CustomPacketPayload.
java
public record ClientboundSummonLightningPayload(BlockPos pos) implements CustomPacketPayload {
public static final Identifier SUMMON_LIGHTNING_PAYLOAD_ID = Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "summon_lightning");
public static final CustomPacketPayload.Type<ClientboundSummonLightningPayload> TYPE = new CustomPacketPayload.Type<>(SUMMON_LIGHTNING_PAYLOAD_ID);
public static final StreamCodec<RegistryFriendlyByteBuf, ClientboundSummonLightningPayload> CODEC = StreamCodec.composite(BlockPos.STREAM_CODEC, ClientboundSummonLightningPayload::pos, ClientboundSummonLightningPayload::new);
@Override
public Type<? extends CustomPacketPayload> type() {
return TYPE;
}
}1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
В то же время мы определили:
- Идентификатор (
Identifier), используемый для распознавания полезной нагрузки нашего пакета. В данном примере наш идентификатор будетexample-mod:summon_lightning.
java
public static final Identifier SUMMON_LIGHTNING_PAYLOAD_ID = Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "summon_lightning");1
- Открытый статический экземпляр (public static)
CustomPayload.Typeдля уникальной идентификации этой пользовательской полезной нагрузки. Мы будем ссылаться на этот ID как в общем коде (common), так и в клиентском (client).
java
public static final CustomPacketPayload.Type<ClientboundSummonLightningPayload> TYPE = new CustomPacketPayload.Type<>(SUMMON_LIGHTNING_PAYLOAD_ID);1
- Открытый статический экземпляр (public static)
StreamCodec, чтобы игра знала, как сериализовать/десериализовать содержимое пакета.
java
public static final StreamCodec<RegistryFriendlyByteBuf, ClientboundSummonLightningPayload> CODEC = StreamCodec.composite(BlockPos.STREAM_CODEC, ClientboundSummonLightningPayload::pos, ClientboundSummonLightningPayload::new);1
Мы также переопределили метод type, чтобы он возвращал ID нашей полезной нагрузки.
java
@Override
public Type<? extends CustomPacketPayload> type() {
return TYPE;
}1
2
3
4
2
3
4
Регистрация полезной нагрузки
Прежде чем отправить пакет с нашей полезной нагрузкой, нам необходимо зарегистрировать её на обеих физических сторонах.
Это можно сделать в нашем общем инициализаторе (common initializer) с помощью метода PayloadTypeRegistry.clientboundPlay().register, который принимает CustomPayload.Type и StreamCodec.
java
PayloadTypeRegistry.clientboundPlay().register(ClientboundSummonLightningPayload.TYPE, ClientboundSummonLightningPayload.CODEC);1
Аналогичный метод существует для регистрации полезной нагрузки, отправляемой от клиента к серверу: PayloadTypeRegistry.serverboundPlay().register.
Отправка пакета клиенту
Чтобы отправить пакет с нашей полезной нагрузкой, мы можем использовать метод ServerPlayNetworking.send, который принимает ServerPlayer и CustomPayload.
Давайте начнем с создания нашего предмета «Молниеносная картошка» (Lightning Tater). Вы можете переопределить метод use, чтобы запускать действие при использовании предмета. В данном случае давайте отправим пакеты игрокам на данном уровне сервера (server level).
java
public class LightningTaterItem extends Item {
public LightningTaterItem(Properties properties) {
super(properties);
}
@Override
public InteractionResult use(Level level, Player user, InteractionHand hand) {
if (level.isClientSide()) {
return InteractionResult.PASS;
}
ClientboundSummonLightningPayload payload = new ClientboundSummonLightningPayload(user.blockPosition());
for (ServerPlayer player : PlayerLookup.level((ServerLevel) level)) {
ServerPlayNetworking.send(player, payload);
}
return InteractionResult.SUCCESS;
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Давайте рассмотрим приведенный выше код.
Мы отправляем пакеты только тогда, когда действие инициировано на сервере, выполняя ранний возврат (return) с помощью проверки isClientSide():
java
if (level.isClientSide()) {
return InteractionResult.PASS;
}1
2
3
2
3
Мы создаем экземпляр полезной нагрузки с позицией пользователя:
java
ClientboundSummonLightningPayload payload = new ClientboundSummonLightningPayload(user.blockPosition());1
Наконец, мы получаем список игроков на уровне сервера через PlayerLookup и отправляем пакет каждому игроку.
java
for (ServerPlayer player : PlayerLookup.level((ServerLevel) level)) {
ServerPlayNetworking.send(player, payload);
}1
2
3
2
3
INFO
Fabric API предоставляет PlayerLookup — набор вспомогательных функций для поиска игроков на сервере.
Термин, часто используемый для описания функционала этих методов — «трекинг» (tracking, отслеживание). Это означает, что объект или чанк на сервере известен клиенту игрока (на расстоянии его обзора), и сущность или блок должны уведомлять отслеживающих клиентов об изменениях.
Трекинг — важная концепция для оптимизации сетевого взаимодействия. Благодаря ему о произошедших изменениях путем отправки пакетов уведомляются только те игроки, которым это необходимо.
Получение пакета на стороне клиента
Чтобы получить пакет, отправленный с сервера на клиент, вам необходимо указать, как именно вы будете обрабатывать входящий пакет.
Это можно сделать в инициализаторе клиента (client initializer), вызвав метод ClientPlayNetworking.registerGlobalReceiver и передав туда CustomPayload.Type и PlayPayloadHandler, который является функциональным интерфейсом.
В данном случае мы определим запускаемое действие внутри реализации PlayPayloadHandler (в виде лямбда-выражения).
java
ClientPlayNetworking.registerGlobalReceiver(ClientboundSummonLightningPayload.TYPE, (payload, context) -> {
ClientLevel level = context.client().level;
if (level == null) {
return;
}
BlockPos lightningPos = payload.pos();
LightningBolt entity = EntityType.LIGHTNING_BOLT.create(level, EntitySpawnReason.TRIGGERED);
if (entity != null) {
entity.setPos(lightningPos.getX(), lightningPos.getY(), lightningPos.getZ());
level.addEntity(entity);
}
});1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Давайте рассмотрим приведенный выше код.
Мы можем получить доступ к данным из нашей полезной нагрузки (payload), вызвав геттеры записи (Record). В данном случае это payload.pos(), который затем можно использовать для получения координат X, Y и Z.
java
BlockPos lightningPos = payload.pos();1
Наконец, мы создаем сущность LightningBolt (молнию) и добавляем ее в мир (level).
java
LightningBolt entity = EntityType.LIGHTNING_BOLT.create(level, EntitySpawnReason.TRIGGERED);
if (entity != null) {
entity.setPos(lightningPos.getX(), lightningPos.getY(), lightningPos.getZ());
level.addEntity(entity);
}1
2
3
4
5
6
2
3
4
5
6
Теперь, если вы добавите этот мод на сервер, то при использовании игроком предмета Lightning Tater все игроки увидят удар молнии в позиции этого пользователя.
Отправка пакета на сервер
Как и в случае с отправкой пакета на клиент, мы начинаем с создания пользовательской полезной нагрузки (custom payload). На этот раз, когда игрок использует "Ядовитый картофель" на живой сущности, мы запрашиваем у сервера наложение на нее эффекта «Свечение» (Glowing).
java
public record GiveGlowingEffectServerboundPayload(int entityId) implements CustomPacketPayload {
public static final Identifier GIVE_GLOWING_EFFECT_PAYLOAD_ID = Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "give_glowing_effect");
public static final CustomPacketPayload.Type<GiveGlowingEffectServerboundPayload> TYPE = new CustomPacketPayload.Type<>(GIVE_GLOWING_EFFECT_PAYLOAD_ID);
public static final StreamCodec<RegistryFriendlyByteBuf, GiveGlowingEffectServerboundPayload> CODEC = StreamCodec.composite(ByteBufCodecs.INT, GiveGlowingEffectServerboundPayload::entityId, GiveGlowingEffectServerboundPayload::new);
@Override
public Type<? extends CustomPacketPayload> type() {
return TYPE;
}
}1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
Мы передаем соответствующий кодек (codec) вместе со ссылкой на метод (method reference) для получения значения из Record для сборки этого кодека.
Затем мы регистрируем нашу полезную нагрузку в общем инициализаторе. Однако на этот раз как пакет от клиента к серверу (Client-to-Server payload), используя метод PayloadTypeRegistry.serverboundPlay().register.
java
PayloadTypeRegistry.serverboundPlay().register(GiveGlowingEffectServerboundPayload.TYPE, GiveGlowingEffectServerboundPayload.CODEC);1
Чтобы отправить пакет, давайте добавим действие при использовании игроком "Ядовитого картофеля". Мы будем использовать событие UseEntityCallback, чтобы код оставался лаконичным.
Мы регистрируем это событие в нашем инициализаторе клиента и используем isClientSide(), чтобы убедиться, что действие будет запущено только на логическом клиенте.
java
UseEntityCallback.EVENT.register((player, level, hand, entity, hitResult) -> {
if (!level.isClientSide()) {
return InteractionResult.PASS;
}
ItemStack usedItemStack = player.getItemInHand(hand);
if (entity instanceof LivingEntity && usedItemStack.is(Items.POISONOUS_POTATO) && hand == InteractionHand.MAIN_HAND) {
GiveGlowingEffectServerboundPayload payload = new GiveGlowingEffectServerboundPayload(hitResult.getEntity().getId());
ClientPlayNetworking.send(payload);
return InteractionResult.SUCCESS;
}
return InteractionResult.PASS;
});1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Мы создаем экземпляр нашего класса GiveGlowingEffectServerboundPayload с необходимыми аргументами. В данном случае это сетевой ID (network ID) целевой сущности.
java
GiveGlowingEffectServerboundPayload payload = new GiveGlowingEffectServerboundPayload(hitResult.getEntity().getId());1
Наконец, мы отправляем пакет на сервер, вызывая метод ClientPlayNetworking.send с экземпляром нашей полезной нагрузки GiveGlowingEffectServerboundPayload.
java
ClientPlayNetworking.send(payload);1
Получение пакета на сервере
Это можно сделать в обычном инициализаторе, вызвав ServerPlayNetworking.registerGlobalReceiver и передав CustomPayload.Type и PlayPayloadHandler.
java
ServerPlayNetworking.registerGlobalReceiver(GiveGlowingEffectServerboundPayload.TYPE, (payload, context) -> {
Entity entity = context.player().level().getEntity(payload.entityId());
if (entity instanceof LivingEntity livingEntity && livingEntity.closerThan(context.player(), 5)) {
livingEntity.addEffect(new MobEffectInstance(MobEffects.GLOWING, 100));
}
});1
2
3
4
5
6
7
2
3
4
5
6
7
INFO
Очень важно проверять содержимое пакета на стороне сервера.
В данном случае мы проверяем, существует ли сущность, на основе ее сетевого ID.
java
Entity entity = context.player().level().getEntity(payload.entityId());1
Кроме того, целевая сущность должна быть живой сущностью (living entity), и мы ограничиваем расстояние от игрока до целевой сущности пятью блоками. Если эти условия будут выполнены, мы применим следующий эффект:
java
if (entity instanceof LivingEntity livingEntity && livingEntity.closerThan(context.player(), 5)) {
livingEntity.addEffect(new MobEffectInstance(MobEffects.GLOWING, 100));
}1
2
3
2
3
Теперь, когда любой игрок попытается использовать "Ядовитый картофель" на живой сущности, к ней применится эффект свечения.

















