注册负载

注册负载

负载(payload)用于在客户端与服务器之间发送任意数据。可以通过 RegisterPayloadHandlersEvent 事件中的 PayloadRegistrar 注册负载。

@SubscribeEvent // on the mod event bus
public static void register(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<>(Identifier.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,用于读写负载数据。

最后,向注册器注册此负载:

// In some common event class

@SubscribeEvent // on the mod event bus
public static void register(RegisterPayloadHandlersEvent event) {
    final PayloadRegistrar registrar = event.registrar("1");
    registrar.playBidirectional(
        MyData.TYPE,
        MyData.STREAM_CODEC,
        ServerPayloadHandler::handleDataOnMain
    );
}

// In some client-only event class

@SubscribeEvent // on the mod event bus only on the physical client
public static void register(RegisterClientPayloadHandlersEvent event) {
    event.register(
        MyData.TYPE,
        ClientPayloadHandler::handleDataOnMain
    );
}

分析上述代码,可以注意到以下几点:

  • 注册器的 play* 方法用于注册在游戏的游玩阶段发送的负载。
    • 代码中没有展示的 configuration* 和 common* 方法也可以用于为配置阶段注册负载。common 方法可以同时为配置阶段和游玩阶段注册负载。
  • 注册器使用 *Bidirectional 方法注册会发往逻辑服务端和逻辑客户端双方的负载。
    • 未展示的 *ToClient 和 *ToServer 方法则分别注册只发往逻辑客户端或逻辑服务端的负载。
  • 负载类型作为该负载的唯一标识符。
  • 流编解码器用于从网络传输的缓冲区中读取负载,以及将负载写入缓冲区。
  • 负载处理器是负载到达某个逻辑端时调用的回调。
    • 使用 *ToServer 方法时,负载处理器是该方法的最后一个参数。
    • 使用 *ToClient 方法时,需要通过 RegisterClientPayloadHandlersEvent 注册处理器,并传入负载类型和处理器。
    • 使用 *Bidirectional 方法时,两种方式都需要使用。

提示 客户端负载处理器使用独立的 RegisterClientPayloadHandlersEvent 事件,避免代码跨越逻辑端与物理端的边界。

注册负载后,需要实现处理器。本例重点介绍客户端处理器,服务端处理器与之非常相似。

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。对于发往客户端的连接,向 RegisterClientPayloadHandlersEvent#register 传入相应 HandlerThread。

// In some common event class

@SubscribeEvent // on the mod event bus
public static void register(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,
        ServerPayloadHandler::handleDataOnNetwork
    );
}

// In some client-only event class

@SubscribeEvent // on the mod event bus only on the physical client
public static void register(RegisterClientPayloadHandlersEvent event) {
    event.register(
        MyData.TYPE,
        HandlerThread.NETWORK // Payload handler will be invoked on the network thread
        ClientPayloadHandler::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 提供了多个向客户端发送负载的便捷实现;ClientPacketDistributor 则提供一个向服务器发送数据包的方法 sendToServer。

// ON THE CLIENT

// Send payload to server
ClientPacketDistributor.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 和 ClientPacketDistributor 类。


来源: NeoForged/Documentation(26.1)。中文翻译依据提交 4a680cdd4d15。