注册载荷
载荷(payload)用于在客户端与服务器之间发送任意数据。通过 RegisterPayloadHandlersEvent 事件提供的 PayloadRegistrar 注册载荷。
@SubscribeEvent // on the mod event bus
public static void register(final RegisterPayloadHandlersEvent event) {
// Sets the current network version
final PayloadRegistrar registrar = event.registrar("1");
}
假设我们要发送以下数据:
public record MyData(String name, int age) {}
可以实现 CustomPacketPayload 接口,创建用于发送和接收这些数据的载荷。
public record MyData(String name, int age) implements CustomPacketPayload {
public static final CustomPacketPayload.Type<MyData> TYPE = new CustomPacketPayload.Type<>(ResourceLocation.fromNamespaceAndPath("mymod", "my_data"));
// Each pair of elements defines the stream codec of the element to encode/decode and the getter for the element to encode
// 'name' will be encoded and decoded as a string
// 'age' will be encoded and decoded as an integer
// The final parameter takes in the previous parameters in the order they are provided to construct the payload object
public static final StreamCodec<ByteBuf, MyData> STREAM_CODEC = StreamCodec.composite(
ByteBufCodecs.STRING_UTF8,
MyData::name,
ByteBufCodecs.VAR_INT,
MyData::age,
MyData::new
);
@Override
public CustomPacketPayload.Type<? extends CustomPacketPayload> type() {
return TYPE;
}
}
如上例所示,CustomPacketPayload 接口要求实现 type 方法,返回该载荷的唯一标识符。还需要通过 StreamCodec 读取和写入载荷数据,以便稍后注册。
最后,将载荷注册到注册器:
@SubscribeEvent // on the mod event bus
public static void register(final RegisterPayloadHandlersEvent event) {
final PayloadRegistrar registrar = event.registrar("1");
registrar.playBidirectional(
MyData.TYPE,
MyData.STREAM_CODEC,
new DirectionalPayloadHandler<>(
ClientPayloadHandler::handleDataOnMain,
ServerPayloadHandler::handleDataOnMain
)
);
}
上述代码有以下几点需要说明:
- 注册器的
play*方法用于注册游戏游玩阶段发送的载荷。- 此例未展示
configuration*和common*方法,它们也可以注册配置阶段的载荷。common方法可以同时为配置和游玩阶段注册载荷。
- 此例未展示
- 注册器使用
*Bidirectional方法,注册可发送到逻辑服务器和逻辑客户端的载荷。- 此例未展示
*ToClient和*ToServer方法,它们分别用于注册只发往逻辑客户端或逻辑服务器的载荷。
- 此例未展示
- 载荷类型用作载荷的唯一标识符。
- 流编解码器用于将载荷写入网络传输的缓冲区,以及从缓冲区读取载荷。
- 载荷处理器是在载荷抵达某个逻辑端时调用的回调。
- 使用
*Bidirectional方法时,可以用DirectionalPayloadHandler为两个逻辑端分别提供处理器。
- 使用
注册载荷后,还需要实现处理器。本例着重介绍客户端处理器,服务器端的实现非常相似。
public class ClientPayloadHandler {
public static void handleDataOnMain(final MyData data, final IPayloadContext context) {
// Do something with the data, on the main thread
blah(data.age());
}
}
注意以下几点:
- 处理方法会收到载荷和上下文对象。
- 默认情况下,载荷处理方法在主线程调用。
如果需要进行资源密集的计算,应在网络线程执行,避免阻塞主线程。在注册载荷前,调用 PayloadRegistrar#executesOn,将注册器的 HandlerThread 设置为 HandlerThread#NETWORK 即可。
@SubscribeEvent // on the mod event bus
public static void register(final RegisterPayloadHandlersEvent event) {
final PayloadRegistrar registrar = event.registrar("1")
.executesOn(HandlerThread.NETWORK); // All subsequent payloads will register on the network thread
registrar.playBidirectional(
MyData.TYPE,
MyData.STREAM_CODEC,
new DirectionalPayloadHandler<>(
ClientPayloadHandler::handleDataOnNetwork,
ServerPayloadHandler::handleDataOnNetwork
)
);
}
提示 在一次
executesOn调用之后注册的所有载荷,都会沿用同一线程执行位置,直到再次调用executesOn。PayloadRegistrar registrar = event.registrar("1"); registrar.playBidirectional(...); // On the main thread registrar.playBidirectional(...); // On the main thread // Configuration methods modify the state of the registrar // by creating a new instance, so the change needs to be /// updated by storing the result registrar = registrar.executesOn(HandlerThread.NETWORK); registrar.playBidirectional(...); // On the network thread registrar.playBidirectional(...); // On the network thread registrar = registrar.executesOn(HandlerThread.MAIN); registrar.playBidirectional(...); // On the main thread registrar.playBidirectional(...); // On the main thread
另有以下几点需要注意:
- 如果要在游戏主线程执行代码,可以使用
enqueueWork向主线程提交任务。- 该方法返回在主线程完成的
CompletableFuture。 - 返回
CompletableFuture意味着可以串联多个任务,并集中处理异常。 - 如果没有处理
CompletableFuture中的异常,异常会被吞掉,你不会收到任何通知。
- 该方法返回在主线程完成的
public class ClientPayloadHandler {
public static void handleDataOnNetwork(final MyData data, final IPayloadContext context) {
// Do something with the data, on the network thread
blah(data.name());
// Do something with the data, on the main thread
context.enqueueWork(() -> {
blah(data.age());
})
.exceptionally(e -> {
// Handle exception
context.disconnect(Component.translatable("my_mod.networking.failed", e.getMessage()));
return null;
});
}
}
定义自己的载荷后,就可以通过配置任务配置客户端和服务器。
发送载荷
CustomPacketPayload 使用原版数据包系统在网络上传输:向服务器发送时包装在 ServerboundCustomPayloadPacket 中,向客户端发送时包装在 ClientboundCustomPayloadPacket 中。发往客户端的载荷最多可包含 1 MiB 数据;发往服务器的载荷必须小于 32 KiB。
所有载荷最终都会通过某种抽象层调用 Connection#send 发送。不过,如果需要按条件发给多名玩家,直接调用这些方法通常不方便。因此,PacketDistributor 提供了许多便捷实现。发往服务器只有一个方法 sendToServer;发往客户端则根据接收玩家的范围,提供了多种方法。
// ON THE CLIENT
// Send payload to server
PacketDistributor.sendToServer(new MyData(...));
// ON THE SERVER
// Send to one player (ServerPlayer serverPlayer)
PacketDistributor.sendToPlayer(serverPlayer, new MyData(...));
/// Send to all players tracking this chunk (ServerLevel serverLevel, ChunkPos chunkPos)
PacketDistributor.sendToPlayersTrackingChunk(serverLevel, chunkPos, new MyData(...));
/// Send to all connected players
PacketDistributor.sendToAllPlayers(new MyData(...));
其他实现请参阅 PacketDistributor 类。
来源: NeoForged/Documentation(1.21.1)。中文翻译依据提交
4a680cdd4d15。