Recent Changes
Media Manager
Sitemap
假如你有根魔杖,可以向附近的所有玩家展示你正在查看的方块。
class HighlightingWandItem extends Item {
public HighlightingWand(Item.Settings settings) {
super(settings)
}
public TypedActionResult<ItemStack> use(World world, PlayerEntity user, Hand hand) {
// 视线追踪并找到你面对的方块
BlockPos target = ...
// 不好的代码:别这么写:
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 这样分发游戏的是为了减少 Minecraft 服务器的 jar 文件的大小。服务器没有理由包含整个渲染引擎,渲染引擎只会在你自己的机器渲染世界时才会用到。在开发环境中,一些类注解了
@Environment(EnvType.CLIENT)
,表示这些类仅存在于客户端。
将数据包发送至游戏客户端
public TypedActionResult<ItemStack> use(World world, PlayerEntity user, Hand hand) {
// 确认我们是否是在逻辑服务器上进行操作
if (world.isClient()) return super.use(world, user, hand);
// 视线追踪并找到玩家朝向的方块
BlockPos target = ...
// 不好的代码:不要这样写!
ClientBlockHighlighting.highlightBlock(MinecraftClient.getInstance(), target);
return TypedActionResult.success(user.getStackInHand(hand));
}
接下来,我们需要将数据包发送到游戏客户端。首先需要定义一个用于识别数据包的
Identifier
。对于本例,我们的标识符为
tutorial:highlight_block
。为将数据包发送到游戏客户端,需要指定要哪个玩家的游戏客户端接收数据包。由于该操作发生在逻辑服务器上,所以可以将
player
向上强转为
ServerPlayerEntity
。
public class TutorialNetworkingConstants {
// 存储数据包的 id 以便后面引用
public static final Identifier HIGHLIGHT_PACKET_ID = Identifier.of("tutorial", "highlight_block");
要将数据包发送到玩家,我们使用 ServerPlayerNetworking
中的一些方法。我们使用该类中的以下方法:
public static void send(ServerPlayerEntity player, Identifier channelName, PacketByteBuf buf) {
数据包将会被发送到此方法中的玩家。通道名称是你之前决定用来识别数据包的 Identifier
。PacketByteBuf
用于存储数据包的数据。然后我们返回,以通过 buf 将数据写入数据包的有效负载。
由于我们没有向数据包写入任何数据,所以现在,我们将发送带有空有效负载的数据包。可以使用 PacketByteBufs.empty()
创建带有空有效负载的 buf。
....
ServerPlayNetworking.send((ServerPlayerEntity) user, TutorialNetworkingConstants.HIGHLIGHT_PACKET_ID, PacketByteBufs.empty());
return TypedActionResult.success(user.getHandStack(hand));
}
虽然你已经向游戏客户端发送了一个数据包,但游戏客户端无法对数据包做任何事情,因为客户端不知道如何接收数据包。关于游戏客户端接收数据包的信息请参见下方:
下面的例子以匿名函数的形式实现玩家通道处理器:
ClientPlayNetworking.registerGlobalReceiver(TutorialNetworkingConstants.HIGHLIGHT_PACKET_ID, (client, handler, buf, responseSender) -> {
...
});
但是,你还不能立即绘制高亮框。这是因为接收器是在网络事件循环中调用的。事件循环在另一个线程上运行,你必须在渲染线程上绘制高亮框。
要绘制高亮框,你需要在游戏客户端上安排任务。这可以由通道处理器提供的 client
字段来完成。通常,你将使用 execute
方法在客户端上运行任务:
ClientPlayNetworking.registerGlobalReceiver(TutorialNetworkingConstants.HIGHLIGHT_PACKET_ID, (client, handler, buf, responseSender) -> {
client.execute(() -> {
// 此 lambda 中的所有内容都在渲染线程上运行
ClientBlockHighlighting.highlightBlock(client, target);
});
});
你可能已经注意到你并不知道要高亮哪个方块。你可以将此数据写入数据包字节 buf。不再在你的物品的 use
方法中向游戏客户端发送 PacketByteBufs.empty()
,而是创建一个新的数据包字节 buf 并将其发送。
PacketByteBuf buf = PacketByteBufs.create();
接下来,您需要将数据写入数据包字节buf。应该注意的是,你必须以和写入数据相同的顺序读取数据。
PacketByteBuf buf = PacketByteBufs.create();
buf.writeBlockPos(target);
之后,你将通过 send
方法发送 buf
字段。
要读取游戏客户端上的方块坐标,你可以使用 PacketByteBuf.readBlockPos()
。
你应该先从网络线程上的数据包中读取所有数据然后再在客户端线程上安排任务。如果你尝试在客户端线程上读取数据,将收到与 ref count 有关的错误。如果一定要在客户端线程上读取数据,则需要 retain()
(保留)这些数据,然后在客户端线程上读取。如果你 retain()
了数据,请确保在不再需要时 release()
(释放)数据。
最后,客户端的处理程序将如下所示:
ClientPlayNetworking.registerGlobalReceiver(TutorialNetworkingConstants.HIGHLIGHT_PACKET_ID, (client, handler, buf, responseSender) -> {
// 在事件循环上读取数据包数据
BlockPos target = buf.readBlockPos();
client.execute(() -> {
// 此 lambda 中的所有内容都在渲染线程上运行
ClientBlockHighlighting.highlightBlock(client, target);
});
});
向服务器发送数据包并在服务器上接收数据包
将数据包发送到服务器并在服务器上接收数据包与在客户端上的操作非常相似。但是,有一些关键的区别。
首先,将数据包发送到服务器是通过 ClientPlayNetworking.send
完成的。在服务器接收数据包与在客户端接收数据包比较类似,使用 ServerPlayNetworking.registerGlobalReceiver(Identifier channelName, ChannelHandler channelHandler)
方法。用于服务器网络通信的 ChannelHandler
也会通过 player
参数传入发送该数据包的 ServerPlayerEntity
(玩家)。
现在,高亮魔杖恰当地使用了网络,因此专用服务器不会崩溃。你邀请你的朋友回到服务器上来炫耀高亮魔杖,你使用魔杖,并且该方块也在你的客户端上高亮了,并且服务器没有崩溃。但是,你的朋友没有看到高亮。这是你在此处已有的代码有意为之的。为解决此问题,看一下物品的 use
代码:
public TypedActionResult<ItemStack> use(World world, PlayerEntity user, Hand hand) {
// 确认我们是否是在逻辑服务器上进行操作
if (world.isClient()) return super.use(world, user, hand);
// 视线追踪并找到玩家朝向的方块
BlockPos target = ...
PacketByteBuf buf = PacketByteBufs.create();
buf.writeBlockPos(target);
ServerPlayNetworking.send((ServerPlayerEntity) user, TutorialNetworkingConstants.HIGHLIGHT_PACKET_ID, buf);
return TypedActionResult.success(user.getHandStack(hand));
}
你可能注意到了,该物品只会将数据包发送给使用了该物品的玩家。为解决此问题,我们可以使用 PlayerLookup
中的实用方法来获取所有可以看到高亮方块的玩家。
因为我们知道高亮会出现在哪个位置,所以我们可以使用 PlayerLookup.tracking(ServerWorld world, BlockPos pos)
来获取所有可以在世界上看到该位置的玩家的集合。然后,您只需在返回的集合中遍历所有玩家,并将数据包发送给每个玩家:
public TypedActionResult<ItemStack> use(World world, PlayerEntity user, Hand hand) {
// 确认我们是否是在逻辑服务器上进行操作
if (world.isClient()) return super.use(world, user, hand);
// 视线追踪并找到玩家朝向的方块
BlockPos target = ...
PacketByteBuf buf = PacketByteBufs.create();
buf.writeBlockPos(target);
// 迭代世界上所有追踪位置的玩家,并将数据包发送给每个玩家
for (ServerPlayerEntity player : PlayerLookup.tracking((ServerWorld) world, target)) {
ServerPlayNetworking.send(player, TutorialNetworkingConstants.HIGHLIGHT_PACKET_ID, buf);
}
return TypedActionResult.success(user.getHandStack(hand));
}
这样修改之后,当你使用魔杖时,你的朋友也应该在他们自己的客户端上看到高亮方块。
自 1.20.5 开始,网络通信的逻辑被大改。在 1.20.5 中,RegistryByteBuf
现在用于游玩阶段网络,你需要定义一个 Payload
。首先,定义一个包含了 BlockPos
的 Payload
:
public record BlockHighlightPayload(BlockPos blockPos) implements CustomPayload {
public static final Id<BlockHighlightPayload> ID = new CustomPayload.Id<>(TutorialNetworkingConstants.HIGHLIGHT_PACKET_ID);
public static final PacketCodec<PacketByteBuf, BlockHighlightPayload> CODEC = PacketCodec.tuple(BlockPos.PACKET_CODEC, BlockHighlightPayload::blockPos, BlockHighlightPayload::new);
// 或者,你也可以这样写:
// public static final PacketCodec<PacketByteBuf, BlockHighlightPayload> CODEC = PacketCodec.of((value, buf) -> buf.writeBlockPos(value.blockPos), buf -> new BlockHighlightPayload(buf.readBlockPos()));
@Override
public Id<? extends CustomPayload> getId() {
return ID;
然后,像这样注册 receiver:
PayloadTypeRegistry.playS2C().register(BlockHighlightPayload.ID, BlockHighlightPayload.CODEC);
ClientPlayNetworking.registerGlobalReceiver(BlockHighlightPayload.ID, (payload, context) -> {
context.client().execute(() -> {
ClientBlockHighlighting.highlightBlock(client, target);
});
});
现在,在服务器一端,你可以像这样把数据包发送给玩家:
public TypedActionResult<ItemStack> use(World world, PlayerEntity user, Hand hand) {
if (world.isClient()) return super.use(world, user, hand);
// ...
for (ServerPlayerEntity player : PlayerLookup.tracking((ServerWorld) world, target)) {
ServerPlayNetworking.send(player, new BlockHighlightPayload(blockPos));
return TypedActionResult.success(user.getHandStack(hand));