🇨🇳 中文 (Chinese - China)
🇨🇳 中文 (Chinese - China)
外观
🇨🇳 中文 (Chinese - China)
🇨🇳 中文 (Chinese - China)
外观
本页面基于这个版本编写:
1.21.4
Minecraft 中的网络用于使客户端和服务端可以相互通信。 网络通信是个广泛的话题, 因此本页面分为几个类别。
数据包是 Minecraft 网络的核心概念。 数据包由任意数据组成,可以从服务器发送到客户端,也可以从客户端发送到服务器。 请参阅下面的图表,它直观地展示了 Fabric 中的网络架构:
请留意数据包是如何充当服务器和客户端之间的桥梁的;这是因为你在游戏中所做的几乎一切都以某种方式涉及网络。 例如,当您发送一条聊天消息时,包含内容的数据包会发送到服务器。 然后,服务器会向所有其他客户端发送包含你消息的另一个数据包。
要记住的一件重要事情是始终有一个服务器在运行,即使在单人游戏和局域网中也是如此。 即使没有其他人和你一起玩,数据包仍用于客户端和服务器之间的通信。 在谈论网络中的端时,我们常用术语“逻辑客户端”和“逻辑服务器”。 集成的单人/局域网服务器和专用服务器都是逻辑服务器,但只有专用服务器可以被视为物理服务器。
当客户端和服务器之间的状态没有同步时,可能会遇到服务器或其他客户端不承认另一个客户端正在做的事情的问题。 这通常被称为“不同步”。 在编写自己的模组时,你可能需要发送数据包来保持服务器和所有客户端的状态同步。
INFO
有效负载是在数据包内发送的数据。
这可以通过创建一个带有实现 CustomPayload
的 BlockPos
参数的 Java Record
来实现。
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;
}
}
同时,我们定义:
Identifier
(标识符)用于识别数据包的有效负载。 在本例中,我们的标识符将是 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
来返回我们的有效载荷ID。
@Override
public Id<? extends CustomPayload> getId() {
return ID;
}
在我们发送带有自定义有效负载的数据包之前,我们需要注册它。
INFO
S2C
和 C2S
是两个常见的后缀,分别表示 服务器到客户端 和 客户端到服务器。
这可以在我们的通用初始化程序中通过使用 PayloadTypeRegistry.playS2C().register
来完成,它接受 CustomPayload.Id
和 PacketCodec
。
PayloadTypeRegistry.playS2C().register(SummonLightningS2CPayload.ID, SummonLightningS2CPayload.CODEC);
存在类似的方法来注册客户端到服务器的有效负载:PayloadTypeRegistry.playC2S().register
。
要发送带有自定义有效负载的数据包,我们可以使用 ServerPlayNetworking.send
,它接收 ServerPlayerEntity
和 CustomPayload
。
让我们从创建 Lightning Tater 物品开始。 您可以重写 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
Fabric API 提供了 PlayerLookup
,这是一组辅助函数,用于在服务器中查找玩家。
经常用来描述这些方法的功能的术语是“跟踪/tracking”。 这意味着服务器上的某个实体或区块为玩家的客户端所知(在其视野距离内),并且该实体或方块实体应将变化通知跟踪客户端。
跟踪是高效网络通信的一个重要概念,这样只有必要的玩家才能通过发送数据包收到变化的通知。
为了在客户端接收从服务器发送的数据包,您需要指定如何处理传入的数据包。
这可以在客户端初始化程序中完成,通过调用 ClientPlayNetworking.registerGlobalReceiver
并传递 CustomPayload.Id
和 PlayPayloadHandler
(一个功能接口)。
本例中,我们将在 PlayPayloadHandler
实现中定义要触发的操作(作为 lambda 表达式)。
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);
}
});
让我们检查一下上面的代码。
我们可以通过调用 Record 的 getter 方法来访问来自有效负载的数据。 本例为 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);
}
现在,如果你将此模组添加到服务器,当玩家使用我们的 Lightning Tater 物品时,每个玩家都会看到闪电击中用户的位置。
就像向客户端发送数据包一样,我们首先创建自定义有效负载。 这次,当玩家对生物实体使用毒马铃薯时,我们会请求服务器对其应用发光效果。
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;
}
}
我们传入适当的编解码器以及方法引用,从 Record 中获取值来构建此编解码器。
然后我们在通用初始化程序中注册我们的有效负载。 然而,这次通过使用 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
的实例。 本例为目标实体的网络 ID。
GiveGlowingEffectC2SPayload payload = new GiveGlowingEffectC2SPayload(hitResult.getEntity().getId());
最后,我们通过使用 GiveGlowingEffectC2SPayload
实例调用 ClientPlayNetworking.send
向服务器发送一个数据包。
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
在服务器端验证数据包的内容非常重要。
在这种情况下,我们根据实体的网络 ID 来验证该实体是否存在。
Entity entity = context.player().getWorld().getEntityById(payload.entityId());
此外,目标实体必须是生物实体,并且我们将目标实体的范围限制在距离玩家 5 的范围内。
if (entity instanceof LivingEntity livingEntity && livingEntity.isInRange(context.player(), 5)) {
现在,当任何玩家尝试对生物实体使用毒马铃薯时,它就会产生发光效果。