网络通信和热键

关于网络通信, ArchitecturyAPI提供了类似Fabric的Packet和类似Forge的Channel这样两种解决方案, 这里笔者将带领读者以两个具体实例切入这两种解决方案, 读者可以实际感受后选择自己喜欢的方式来进行网络通信.

使用NetworkManager进行网络通信

本节将使用类似Fabric的NetworkManager来写一个处理客户端鼠标滚动事件的实例. 这里笔者给上文我们提到的经验储存器添加了一个小功能, 可以让玩家用Shift+滚动来调整单次经验的存取量.

很显然, 我们的需求涉及一个C2S的网络包请求. 熟悉模组开发的读者应该清楚, 滚轮事件是由客户端发出的, 其产生的影响 (这里是改变经验储存器的一个NBT值) 是体现在逻辑服务端的. 一个携带数据的网络包需要从玩家的客户端发出, 到达逻辑服务器产生影响.

按照需求, 我们在trou.arch下创建network包, 并且创建ModPackets类

位于common: trou/arch/network/ModPackets.java
public class ModPackets {
    public static final ResourceLocation EXP_CONTAINER_MODE = new ResourceLocation("arch", "exp_container_change_mode");

    public static void register() {
        NetworkManager.registerReceiver(NetworkManager.Side.C2S, EXP_CONTAINER_MODE, (buf, context) -> {
            Player player = context.getPlayer();
            int deltaMode = buf.readInt();
            ItemStack stack = player.getItemInHand(InteractionHand.MAIN_HAND);
            ItemExpContainer.raiseMode(stack, deltaMode);
        });
    }

    public static void sendExpContainerMousePacket(int deltaMode) {
        FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.buffer());
        buf.writeInt(deltaMode);
        NetworkManager.sendToServer(EXP_CONTAINER_MODE, buf);
    }
}

FriendlyByteBuf是针对ByteBuf的一个封装, 其中包含了常用的Minecraft对象的序列化和反序列化, 读者需要时可以自行查看

需要注意的是, 由于ByteBuf的特性, 数据被写入的顺序和读取的顺序必须一致

显然, 我们调用了NetworkManager提供的方法并且完成了包的发送和接收, 包的数据由一个ByteBuf携带, 相信读者已经对这种模式不陌生了.

注册包的接收端时需要提供Side, 由于我们是由逻辑客户端发向逻辑服务端, 所以我们选择Side.C2S

context包含了一些随包携带的常用参数, 包含发起的玩家和Environment, 方便我们直接进行使用和判断.

最后, 不要忘记在主类中调用我们ModPackets的register方法

处理raiseMode方法

接下来我们要处理逻辑服务端触发的raiseMode方法, 我们希望通过这个方法改变手中经验储存器的单次存取量.

相信不用笔者过多赘述, 读者可以理解其中的逻辑.

我们还需要修改存取经验时的逻辑, 使其按照我们设定的数值进行存取

最后使单次存取量可以在Tooltip显示, 并添加相关i18n词条

监听鼠标滚动事件

接下来我们来完成监听鼠标滚轮的相关事件, 相信读过上一节的读者已经对此有头绪了. 经过寻找, 我们命中了ClientRawInputEvent.MOUSE_SCROLLED事件, 这个事件会在玩家进入世界后滚动滚轮触发, 并且在打开GUI等界面时不会触发, 完美符合我们的需求.

修改ModEvents类来监听这个事件, 并且在ItemExpContainer类中编写事件处理器

显然 ClientRawInputEvent 只会在客户端进入世界后触发, 我们可以直接用 minecraft.player 来拿到非空的本地玩家

EventResult.interruptFalse() 会阻碍接下来滚轮的滚动, 使事件冒泡停止

EventResult.pass() 不会影响接下来滚轮的滚动

到这里, 我们的功能已经写好了, 读者可以启动游戏验证我们的代码, 确保其在Forge和Fabric均能正常使用

经验储存器

使用NetworkChannel进行网络通信

本节我们将使用类似Forge的NetworkChannel进行网络通信, 做出一个点击热键即可将储存器中的经验丢给盯着的玩家的功能

NetworkChannel的核心概念是Channel和Message, 当网络通信时, 我们的视觉效果是Message在Channel中传递, 显然, 丢经验这个动作应该属于一条Message

所以我们在network包中创建MessageThrowExp类, 来诠释丢经验这条消息

请永远使用UUID来作为玩家的标识符, 而不要传递Player对象或者玩家的名字

显然, 这里我们要承载的数据是指向的玩家的UUID, 我们需要编写一个构造器来提供成员变量的值, 并且在encode时将值编码到ByteBuf中

apply方法会在ByteBuf的值序列化到实例中后被逻辑服务器调用, 这里可以处理我们的逻辑. 与上文中的context相类似, contextSupplier会提供Player, Environment等可能用到的上下文对象.

完成了Message, 接下来我们需要定义并注册承载它的Channel, 在network包下创建ModChannels类

根据读者对Channel的不同理解, 你可以在一个Mod使用多个Channel承载不同功能, 也可以只使用一个Channel来承载该Mod所有的Message, 这只是设计模式的差别, 读者可以根据自己的喜好来决定.

可以看到这里笔者创建的Channel的标识符中提到了exp_container, 即为经验储存器创建了一个单独的Channel

最后, 我们需要在主类中调用ModChannels.register()方法

处理doThrow方法

相信这里不必笔者多说, 读者应该已经想出了相关逻辑

当执行这个方法的时候, 会从经验提取器中提取指定量的经验, 给予目标玩家

添加一个热键

ArchitecturyAPI为我们提供了通用的添加热键的方式.

创建trou.arch.client包, 并创建ModKeys类, 创建我们的KeyMapping

KeyMapping的构造器需要四个参数

  1. 热键的本地化名称: 将显示在选项-控制中

  2. 热键的类型: 可选值为 KEYSYM, SCANCODE, MOUSE. 分别对应键盘, 组合键, 鼠标

  3. 热键的值: 具体取决于类型, 这里InputConstants.KEY_Z代表键盘的Z键

  4. 热键类别的本地化名称: 也将显示在选项-控制中

接下来我们要为热键赋予功能, 根据ArchitecturyAPI文档中的介绍, 我们要监听ClientTickEvent, 并且使用一个while来判断热键是否被按下.

ArchitecturyAPI文档

这里我们在ModEvents中监听事件, 并且编写事件处理器

最后, 我们在主类中调用ModKeys.register方法来注册热键, 为了防止问题, 我们让热键在事件之前注册

此时进入游戏, 你应该能看到添加的热键了, 并且能够使用热键给玩家丢经验了

总结

阅读上文的代码后, 你的主类应该是这样的

本章我们主要介绍了两种网络通信的方式, 使用NetworkManager较为简便, 使用NetworkChannel较为灵活, 这里读者可以根据自己的需求来选择.

如果你对Fabric熟悉, 或需求较为单一, 推荐使用NetworkManager

如果你对Forge熟悉, 或需求较为复杂, 希望可读性强一些, 推荐使用NetworkChannel

最后更新于