🇺🇦 Українська (Ukrainian - Ukraine)
🇺🇦 Українська (Ukrainian - Ukraine)
Зовнішній вигляд
🇺🇦 Українська (Ukrainian - Ukraine)
🇺🇦 Українська (Ukrainian - Ukraine)
Зовнішній вигляд
Ця сторінка написана для версії:
1.21.4
Мережа в Minecraft використовується для того, щоб клієнт і сервер могли спілкуватися один з одним. Мережа– це широка тема, тому ця сторінка поділена на кілька категорій.
Важливість мережі можна показати на простому прикладі коду.
WARNING
Наведений нижче код призначений лише для демонстрації.
Скажімо, у вас є жезл, який підсвічує блок, який ви шукаєте, і який буде видно всім найближчі гравці:
public class HighlightingWandItem extends Item {
public HighlightingWandItem(Item.Settings settings) {
super(settings);
}
public TypedActionResult<ItemStack> use(World world, PlayerEntity user, Hand hand) {
BlockPos target = ...
// BAD CODE: DON'T EVER DO THIS!
ClientBlockHighlighting.highlightBlock(MinecraftClient.getInstance(), target);
return super.use(world, user, hand);
}
}
Під час тестування ви побачите, як викликається блискавка, і нічого не ламається. Тепер ви хочете показати мод своєму друже, ви завантажуєте виділений сервер і запрошуєте свого друга з установленим модом.
Ви використовуєте предмет, і сервер виходить з ладу. Ймовірно, ви помітите в журналі збоїв подібну помилку:
[Server thread/FATAL]: Error executing task on Server
java.lang.RuntimeException: Cannot load class net.minecraft.client.MinecraftClient in environment type SERVER
Код викликає логіку, присутню лише в клієнтському дистрибутиві Minecraft. Причина розповсюдження Mojang гра таким чином полягає в тому, щоб зменшити розмір JAR-файлу Minecraft Server. Насправді немає причин включати весь механізм візуалізації, коли ваша власна машина візуалізує світ.
У середовищі розробки клієнтські класи позначаються анотацією @Environment(EnvType.CLIENT)
.
Щоб розв'язати цю проблему, вам потрібно зрозуміти, як взаємодіють ігровий клієнт і виділений сервер:
На діаграмі вище показано, що ігровий клієнт і виділений сервер є окремими системами, з’єднаними разом за допомогою моста пакети. Пакети можуть містити дані, які ми називаємо корисним навантаженням.
Цей пакетний міст існує не лише між ігровим клієнтом і виділеним сервером, а й між вашим клієнтом і інший клієнт, підключений через локальну мережу. Пакетний міст також присутній навіть в грі наодинці. Це пояснюється тим, що ігровий клієнт запускає спеціальний інтегрований екземпляр сервера для запуску гри.
Підключення до сервера через локальну мережу або гру наодинці також можна розглядати як сервер як віддалений виділений сервер; так ваш ігровий клієнт не може отримати прямий доступ до примірника сервера.
Це можна зробити, створивши Java Record
з параметром BlockPos
, який реалізує CustomPayload
.
public record SummonLightningS2CPayload(BlockPos pos) implements CustomPayload {
public static final Identifier SUMMON_LIGHTNING_PAYLOAD_ID = Identifier.of(FabricDocsReference.MOD_ID, "summon_lightning");
public static final CustomPayload.Id<SummonLightningS2CPayload> ID = new CustomPayload.Id<>(SUMMON_LIGHTNING_PAYLOAD_ID);
public static final PacketCodec<RegistryByteBuf, SummonLightningS2CPayload> CODEC = PacketCodec.tuple(BlockPos.PACKET_CODEC, SummonLightningS2CPayload::pos, SummonLightningS2CPayload::new);
@Override
public Id<? extends CustomPayload> getId() {
return ID;
}
}
Водночас ми визначили:
Ідентифікатор
, який використовується для ідентифікації корисного навантаження нашого пакета. Для цього прикладу нашим ідентифікатором буде fabric-docs-reference:summon_lightning
.public static final Identifier SUMMON_LIGHTNING_PAYLOAD_ID = Identifier.of(FabricDocsReference.MOD_ID, "summon_lightning");
CustomPayload.Id
для унікальної ідентифікації цього спеціального корисного навантаження. Ми будемо посилатися на це ID як у нашому загальному, так і в клієнтському коді.public static final CustomPayload.Id<SummonLightningS2CPayload> ID = new CustomPayload.Id<>(SUMMON_LIGHTNING_PAYLOAD_ID);
PacketCodec
, щоб гра знала, як серіалізувати/десеріалізувати вміст пакету.public static final PacketCodec<RegistryByteBuf, SummonLightningS2CPayload> CODEC = PacketCodec.tuple(BlockPos.PACKET_CODEC, SummonLightningS2CPayload::pos, SummonLightningS2CPayload::new);
Ми також замінили getId
, щоб повернути наш ідентифікатор корисного навантаження.
@Override
public Id<? extends CustomPayload> getId() {
return ID;
}
Перш ніж надіслати пакет із нашим власним корисним навантаженням, нам потрібно його зареєструвати.
INFO
S2C
і C2S
— два загальні суфікси, які означають Server-to-Client і Client-to-Server відповідно.
Це можна зробити в нашому загальному ініціалізаторі за допомогою PayloadTypeRegistry.playS2C().register
, який приймає CustomPayload.Id
і PacketCodec
.
PayloadTypeRegistry.playS2C().register(SummonLightningS2CPayload.ID, SummonLightningS2CPayload.CODEC);
Подібний метод існує для реєстрації корисних даних клієнт-сервер: PayloadTypeRegistry.playC2S().register
.
Щоб надіслати пакет із нашим власним корисним навантаженням, ми можемо використати ServerPlayNetworking.send
, який приймає ServerPlayerEntity
і CustomPayload
.
Почнемо зі створення нашого предмета, блискавичного татера. Ви можете замінити use
, щоб активувати дію під час використання елемента. У цьому випадку надішлімо пакети гравцям у світі серверів.
public class LightningTaterItem extends Item {
public LightningTaterItem(Settings settings) {
super(settings);
}
@Override
public ActionResult use(World world, PlayerEntity user, Hand hand) {
if (world.isClient()) {
return ActionResult.PASS;
}
SummonLightningS2CPayload payload = new SummonLightningS2CPayload(user.getBlockPos());
for (ServerPlayerEntity player : PlayerLookup.world((ServerWorld) world)) {
ServerPlayNetworking.send(player, payload);
}
return ActionResult.SUCCESS;
}
}
Розгляньмо код вище.
Ми надсилаємо пакети лише тоді, коли дію розпочато на сервері, повертаючись раніше з перевіркою isClient
:
if (world.isClient()) {
return ActionResult.PASS;
}
Ми створюємо екземпляр корисного навантаження з позицією користувача:
SummonLightningS2CPayload payload = new SummonLightningS2CPayload(user.getBlockPos());
Нарешті, ми отримуємо гравців у світі сервера через PlayerLookup
і надсилаємо пакет кожному гравцеві.
for (ServerPlayerEntity player : PlayerLookup.world((ServerWorld) world)) {
ServerPlayNetworking.send(player, payload);
}
INFO
API Fabric надає PlayerLookup
, набір допоміжних функцій, які шукатимуть гравців на сервері.
Для опису функціональності цих методів часто використовується термін «відстеження». Це означає, що сутність або блок на сервері відомі клієнту гравця (в межах їх видимості), і сутність або блокова сутність повинні повідомити відстеження змін клієнтів.
Відстеження є важливою концепцією для ефективної роботи в мережі, щоб лише необхідні гравці отримували повідомлення про зміни шляхом надсилання пакетів.
Щоб отримати пакет, надісланий із сервера на клієнті, вам потрібно вказати, як ви будете обробляти вхідний пакет.
Це можна зробити в ініціалізаторі клієнта, викликавши ClientPlayNetworking.registerGlobalReceiver
і передавши CustomPayload.Id
і PlayPayloadHandler
, який є функціональним інтерфейсом.
У цьому випадку ми визначимо дію, яка буде запускатися в реалізації реалізації PlayPayloadHandler
(як лямбда-вираз).
ClientPlayNetworking.registerGlobalReceiver(SummonLightningS2CPayload.ID, (payload, context) -> {
ClientWorld world = context.client().world;
if (world == null) {
return;
}
BlockPos lightningPos = payload.pos();
LightningEntity entity = EntityType.LIGHTNING_BOLT.create(world, SpawnReason.TRIGGERED);
if (entity != null) {
entity.setPosition(lightningPos.getX(), lightningPos.getY(), lightningPos.getZ());
world.addEntity(entity);
}
});
Розгляньмо код вище.
Ми можемо отримати доступ до даних із нашого корисного навантаження, викликавши методи отримання запису. У цьому випадку payload.pos()
. Який потім можна використовувати для отримання позицій "x", "y" і "z".
BlockPos lightningPos = payload.pos();
Нарешті ми створюємо LightningEntity
і додаємо його до світу.
LightningEntity entity = EntityType.LIGHTNING_BOLT.create(world, SpawnReason.TRIGGERED);
if (entity != null) {
entity.setPosition(lightningPos.getX(), lightningPos.getY(), lightningPos.getZ());
world.addEntity(entity);
}
Тепер, якщо ви додасте цей мод на сервер і коли гравець використовує наш блискавичний татер, кожен гравець побачить блискавку вражаючи позицію користувача.
Подібно до надсилання пакета клієнту, ми починаємо зі створення спеціального корисного навантаження. Цього разу, коли гравець використовує отруйну картоплю на живій істоті, ми просимо сервер застосувати до неї ефект сяяння.
public record GiveGlowingEffectC2SPayload(int entityId) implements CustomPayload {
public static final Identifier GIVE_GLOWING_EFFECT_PAYLOAD_ID = Identifier.of(FabricDocsReference.MOD_ID, "give_glowing_effect");
public static final CustomPayload.Id<GiveGlowingEffectC2SPayload> ID = new CustomPayload.Id<>(GIVE_GLOWING_EFFECT_PAYLOAD_ID);
public static final PacketCodec<RegistryByteBuf, GiveGlowingEffectC2SPayload> CODEC = PacketCodec.tuple(PacketCodecs.INTEGER, GiveGlowingEffectC2SPayload::entityId, GiveGlowingEffectC2SPayload::new);
@Override
public Id<? extends CustomPayload> getId() {
return ID;
}
}
Ми передаємо відповідний кодек разом із посиланням на метод, щоб отримати значення із запису для створення цього кодека.
Потім ми реєструємо наше корисне навантаження в нашому загальному ініціалізаторі. Однак цього разу як корисне навантаження клієнт-сервер за допомогою PayloadTypeRegistry.playC2S().register
.
PayloadTypeRegistry.playC2S().register(GiveGlowingEffectC2SPayload.ID, GiveGlowingEffectC2SPayload.CODEC);
Щоб надіслати пакет, додаймо дію, коли гравець використовує отруйну картоплю. Ми будемо використовувати UseEntityCallback
подія, щоб зберегти речі стисло.
Ми реєструємо подію в нашому ініціалізаторі клієнта та використаємо isClient()
, щоб гарантувати, що дія лише запускається на логічному клієнті.
UseEntityCallback.EVENT.register((player, world, hand, entity, hitResult) -> {
if (!world.isClient()) {
return ActionResult.PASS;
}
ItemStack usedItemStack = player.getStackInHand(hand);
if (entity instanceof LivingEntity && usedItemStack.isOf(Items.POISONOUS_POTATO) && hand == Hand.MAIN_HAND) {
GiveGlowingEffectC2SPayload payload = new GiveGlowingEffectC2SPayload(hitResult.getEntity().getId());
ClientPlayNetworking.send(payload);
return ActionResult.SUCCESS;
}
return ActionResult.PASS;
});
Ми створимо екземпляр нашого GiveGlowingEffectC2SPayload
з необхідними аргументами. У цьому випадку ідентифікатор мережі з цільової сутності.
GiveGlowingEffectC2SPayload payload = new GiveGlowingEffectC2SPayload(hitResult.getEntity().getId());
Нарешті, ми надсилаємо пакет на сервер, викликаючи ClientPlayNetworking.send
з примірником нашого GiveGlowingEffectC2SPayload
.
ClientPlayNetworking.send(payload);
Це можна зробити в загальному ініціалізаторі, викликавши ServerPlayNetworking.registerGlobalReceiver
і передавши CustomPayload.Id
і PlayPayloadHandler
.
ServerPlayNetworking.registerGlobalReceiver(GiveGlowingEffectC2SPayload.ID, (payload, context) -> {
Entity entity = context.player().getWorld().getEntityById(payload.entityId());
if (entity instanceof LivingEntity livingEntity && livingEntity.isInRange(context.player(), 5)) {
livingEntity.addStatusEffect(new StatusEffectInstance(StatusEffects.GLOWING, 100));
}
});
INFO
Важливо, щоб ви перевірили вміст пакета на стороні сервера.
У цьому випадку ми перевіряємо, чи існує сутність, на основі її ідентифікатора мережі.
Entity entity = context.player().getWorld().getEntityById(payload.entityId());
Крім того, цільова сутність має бути живою істотою, і ми обмежуємо діапазон цільової сутності від гравця до 5.
if (entity instanceof LivingEntity livingEntity && livingEntity.isInRange(context.player(), 5)) {
Тепер, коли будь-який гравець намагатиметься використати отруйну картоплю на живій істоті, до нього буде застосовано ефект світіння.